Skip to content

Commit

Permalink
refactor(command): refactor help command
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed Jun 24, 2020
1 parent a1d61c9 commit d3c2fa1
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 235 deletions.
211 changes: 12 additions & 199 deletions packages/command/commands/help.ts
@@ -1,213 +1,26 @@
import { encode } from 'https://deno.land/std@v0.52.0/encoding/utf8.ts';
import { blue, bold, dim, magenta, red, yellow } from 'https://deno.land/std@v0.52.0/fmt/colors.ts';
import { IFlagOptions, IFlags } from '../../flags/lib/types.ts';
import { Table } from '../../table/lib/table.ts';
import format from '../../x/format.ts';
import { ArgumentsParser } from '../lib/arguments-parser.ts';
import { IFlags } from '../../flags/lib/types.ts';
import { BaseCommand } from '../lib/base-command.ts';
import { IEnvVariable, IHelpCommand, IOption } from '../lib/types.ts';
import { CommandListType } from '../types/command-list.ts';
import { ParentCommandListType } from '../types/parent-command-list.ts';

/**
* Generates well formatted and colored help output for specified command.
*/
export class HelpCommand extends BaseCommand implements IHelpCommand {

public constructor( protected parent: BaseCommand ) {
export class HelpCommand extends BaseCommand {

public constructor( cmd?: BaseCommand ) {
super();

this
.type( 'command', new CommandListType() )
this.type( 'command', new ParentCommandListType() )
.arguments( '[command:command]' )

.description( 'Show this help or the help of a sub-command.' )

.action( ( flags: IFlags, name?: string ) => {
this.show( name );
Deno.exit( 0 );
} );
}

public show( name?: string ) {
Deno.stdout.writeSync( encode( this.getHelp( name ) ) );
}

/**
* Render help output.
*/
public getHelp( name?: string ): string {

const cmd: BaseCommand | undefined = name ? this.parent.getCommand( name, false ) : this.parent;

if ( !cmd ) {
throw this.error( new Error( `Sub-command not found: ${ name }` ) );
}

let output = '';
const indent = 2;

const renderHelp = () => {

// Header
renderLine();
output += Table.from( getHeader() )
.indent( indent )
.padding( 1 )
.toString();

// Description
if ( cmd.getDescription() ) {
renderLabel( 'Description' );
output += Table.from( getDescription() )
.indent( indent * 2 )
.maxCellWidth( 140 )
.padding( 1 )
.toString();
}

// Options
if ( cmd.hasOptions() ) {
renderLabel( 'Options' );
output += Table.from( getOptions() )
.padding( [ 2, 2, 1, 2 ] )
.indent( indent * 2 )
.maxCellWidth( [ 60, 60, 80, 60 ] )
.toString();
}

// Commands
if ( cmd.hasCommands( false ) ) {
renderLabel( 'Commands' );
output += Table.from( getCommands() )
.padding( [ 2, 2, 1, 2 ] )
.indent( indent * 2 )
.toString();
}

// Environment variables
if ( cmd.hasEnvVars( false ) ) {
renderLabel( 'Environment variables' );
output += Table.from( getEnvVars() )
.padding( 2 )
.indent( indent * 2 )
.toString();
}

// Examples
if ( cmd.hasExamples() ) {
renderLabel( 'Examples' );
output += Table.from( getExamples() )
.padding( 1 )
.indent( indent * 2 )
.maxCellWidth( 150 )
.toString();
}

renderLine();
};

const renderLine = ( ...args: any[] ) => output += ( args.length ? ' '.repeat( indent ) + format( ...args ) : '' ) + '\n';

const renderLabel = ( label: string ) => {
renderLine();
renderLine( bold( `${ label }:` ) );
renderLine();
};

const getHeader = (): string[][] => {

return [
[ bold( 'Usage:' ), magenta( `${ cmd.getName() }${ cmd.getArgsDefinition() ? ' ' + cmd.getArgsDefinition() : '' }` ) ],
[ bold( 'Version:' ), yellow( `v${ cmd.getVersion() }` ) ]
];
};

const getDescription = (): string[][] => {

return [
[ cmd.getDescription() ]
];
};

const getOptions = (): string[][] => {

return [
...cmd.getOptions( false ).map( ( option: IOption ) => [
option.flags.split( /,? +/g ).map( flag => blue( flag ) ).join( ', ' ),
ArgumentsParser.highlightArguments( option.typeDefinition || '' ),
red( bold( '-' ) ),
option.description.split( '\n' ).shift() as string,
getHints( option )
] )
];
};

const getCommands = (): string[][] => {

return [
...cmd.getCommands( false ).map( ( command: BaseCommand ) => [
[ command.getName(), ...command.getAliases() ].map( name => blue( name ) ).join( ', ' ),
ArgumentsParser.highlightArguments( command.getArgsDefinition() || '' ),
red( bold( '-' ) ),
command.getDescription().split( '\n' ).shift() as string
] )
];
};

const getEnvVars = (): string[][] => {

return [
...cmd.getEnvVars( false ).map( ( envVar: IEnvVariable ) => [
envVar.names.map( name => blue( name ) ).join( ', ' ),
ArgumentsParser.highlightArgumentDetails( envVar.details ),
`${ red( bold( '-' ) ) } ${ envVar.description }`
] )
];
};

const getExamples = (): string[][] => {

let first = true;
const rows: string[][] = [];
cmd.getExamples().map( example => {
if ( !first ) {
rows.push( [] );
if ( !cmd ) {
cmd = name ? this.getGlobalParent()?.getBaseCommand( name ) : this.getGlobalParent();
}
if ( !cmd ) {
throw new Error( `Failed to generate help for command '${ name }'. Command not found.` );
}
first = false;
rows.push( [
dim( bold( `${ capitalize( example.name ) }:` ) ),
`\n${ example.description }`
] );
cmd.help();
Deno.exit( 0 );
} );

return rows;
};

const getHints = ( option: IFlagOptions ): string => {
const hints = [];

option.required && hints.push( yellow( `required` ) );
typeof option.default !== 'undefined' && hints.push( blue( bold( `Default: ` ) ) + blue( format( option.default ) ) );
option.depends && option.depends.length && hints.push( red( bold( `depends: ` ) ) + option.depends.map( depends => red( depends ) ).join( ', ' ) );
option.conflicts && option.conflicts.length && hints.push( red( bold( `conflicts: ` ) ) + option.conflicts.map( conflict => red( conflict ) ).join( ', ' ) );

if ( hints.length ) {
return `(${ hints.join( ', ' ) })`;
}

return '';
};

renderHelp();

return output;
}
}

function capitalize( string: string ): string {
if ( !string ) {
return '';
}
return string.charAt( 0 ).toUpperCase() + string.slice( 1 );
}
28 changes: 10 additions & 18 deletions packages/command/lib/base-command.ts
Expand Up @@ -2,15 +2,16 @@ const { stdout, stderr } = Deno;
import { encode } from 'https://deno.land/std@v0.52.0/encoding/utf8.ts';
import { dim, red } from 'https://deno.land/std@v0.52.0/fmt/colors.ts';
import { parseFlags } from '../../flags/lib/flags.ts';
import { IFlagArgument, IFlagOptions, IFlagsResult, IFlagValue, IFlagValueHandler, IFlagValueType, ITypeHandler, OptionType } from '../../flags/lib/types.ts';
import { IFlagArgument, IFlagOptions, IFlagsResult, IFlagValue, IFlagValueHandler, IFlagValueType, ITypeHandler } from '../../flags/lib/types.ts';
import { fill } from '../../flags/lib/utils.ts';
import format from '../../x/format.ts';
import { BooleanType } from '../types/boolean.ts';
import { NumberType } from '../types/number.ts';
import { StringType } from '../types/string.ts';
import { Type } from '../types/type.ts';
import { ArgumentsParser } from './arguments-parser.ts';
import { IAction, IArgumentDetails, ICommandOption, ICompleteHandler, ICompleteOptions, ICompleteSettings, IDescription, IEnvVariable, IEnvVarOption, IExample, IHelpCommand, IOption, IParseResult, isHelpCommand, ITypeMap, ITypeOption, ITypeSettings } from './types.ts';
import { HelpGenerator } from './help-generator.ts';
import { IAction, IArgumentDetails, ICommandOption, ICompleteHandler, ICompleteOptions, ICompleteSettings, IDescription, IEnvVariable, IEnvVarOption, IExample, IOption, IParseResult, ITypeMap, ITypeOption, ITypeSettings } from './types.ts';

const permissions: any = ( Deno as any ).permissions;
const envPermissionStatus: any = permissions && permissions.query && await permissions.query( { name: 'env' } );
Expand Down Expand Up @@ -1415,25 +1416,16 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
}

/**
* Execute help command.
* Output generated help without exiting.
*/
public help() {

this.getHelpCommand().show();
Deno.stdout.writeSync( encode( this.getHelp() ) );
}

public getHelpCommand(): IHelpCommand {

const helpCommand = this.getCommand( 'help', true );

if ( !helpCommand ) {
throw this.error( new Error( `No help command registered.` ), false );
}

if ( !isHelpCommand( helpCommand ) ) {
throw this.error( new Error( `The registered help command does not correctly implement interface IHelpCommand.` ), false );
}

return helpCommand;
/**
* Get generated help.
*/
public getHelp(): string {
return HelpGenerator.generate( this );
}
}
2 changes: 1 addition & 1 deletion packages/command/lib/default-command.ts
Expand Up @@ -32,7 +32,7 @@ export class DefaultCommand<O = any, A extends Array<any> = any> extends BaseCom
}
} )

.command( 'help', new HelpCommand( this ) )
.command( 'help', new HelpCommand() )

.reset();
}
Expand Down

0 comments on commit d3c2fa1

Please sign in to comment.