Skip to content

Commit

Permalink
feat(command): add support for global option's (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed May 28, 2020
1 parent eb3f578 commit 7d6e7cf
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 35 deletions.
20 changes: 20 additions & 0 deletions examples/command/global-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env -S deno run

import { Command } from '../../packages/command/lib/command.ts';

await new Command()
.version( '0.1.0' )
.option( '-l, --local [val:string]', 'Only available on this command.' )
.option( '-g, --global [val:string]', 'Available on this and all nested child command\'s.', { global: true } )
.action( console.log )

.command( 'command1', new Command()
.description( 'Some sub command.' )
.action( console.log )

.command( 'command2', new Command()
.description( 'Some nested sub command.' )
.action( console.log )
)
)
.parse( Deno.args );
33 changes: 33 additions & 0 deletions packages/command/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
- [Default option value](#default-option-value)
- [Required options](#required-options)
- [Negatable options](#negatable-options)
- [Global options](#global-options)
- [Options which depends on other options](#options-which-depends-on-other-options)
- [Options which conflicts with other options](#options-which-conflicts-with-other-options)
- [Custom option processing](#custom-option-processing)
Expand Down Expand Up @@ -454,6 +455,38 @@ $ deno run https://deno.land/x/cliffy/examples/command/negatable-options.ts --ch
You ordered a pizza with sauce and parmesan cheese
```

### Global options

To share options with child commands with the `global` option.

```typescript
#!/usr/bin/env -S deno run

import { Command } from 'https://deno.land/x/cliffy/command.ts';

await new Command()
.version( '0.1.0' )
.option( '-l, --local [val:string]', 'Only available on this command.' )
.option( '-g, --global [val:string]', 'Available on this and all nested child command\'s.', { global: true } )
.action( console.log )

.command( 'command1', new Command()
.description( 'Some sub command.' )
.action( console.log )

.command( 'command2', new Command()
.description( 'Some nested sub command.' )
.action( console.log )
)
)
.parse( Deno.args );
```

```
$ deno run https://deno.land/x/cliffy/examples/command/global-options.ts command1 command2 -g test
{ global: "test" }
```

### Options which depends on other options

Some options can not be call without other options. You can specify depending options with the `depends` option.
Expand Down
10 changes: 5 additions & 5 deletions packages/command/commands/completions/zsh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,12 @@ function _${ snakeCase( command.getPath() ) }() {`
const baseName: string = cmdArgs.shift() as string;
const completionsPath: string = cmdArgs.join( ' ' );

const excluded: string[] = command.getOptions()
.map( option => option.standalone ? option.flags.split( /[, ] */g ) : false )
.flat()
.filter( flag => typeof flag === 'string' ) as string[];
const excluded: string[] = command.getOptions( false )
.map( option => option.standalone ? option.flags.split( /[, ] */g ) : false )
.flat()
.filter( flag => typeof flag === 'string' ) as string[];

for ( const option of command.getOptions() ) {
for ( const option of command.getOptions( false ) ) {

const optExcluded = option.conflicts ? [ ...excluded, ...option.conflicts ] : excluded;

Expand Down
2 changes: 1 addition & 1 deletion packages/command/commands/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class HelpCommand extends BaseCommand implements IHelpCommand {
const getOptions = (): string[][] => {

return [
...cmd.getOptions().map( ( option: IOption ) => [
...cmd.getOptions( false ).map( ( option: IOption ) => [
option.flags.split( /,? +/g ).map( flag => blue( flag ) ).join( ', ' ),
this.highlight( option.typeDefinition || '' ),
red( bold( '-' ) ),
Expand Down
120 changes: 93 additions & 27 deletions packages/command/lib/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
* @param desc Flag description.
* @param opts Flag options.
*/
public option( flags: string, desc: string, opts?: ICommandOption<O, A> ): this;
public option( flags: string, desc: string, opts?: ICommandOption<O, A> | IFlagValueHandler ): this {
public option( flags: string, desc: string, opts?: ICommandOption ): this;
public option( flags: string, desc: string, opts?: ICommandOption | IFlagValueHandler ): this {

if ( typeof opts === 'function' ) {
return this.option( flags, desc, { value: opts } );
Expand All @@ -347,7 +347,7 @@ export class BaseCommand<O = any, A extends Array<any> = any> {

const args: IArgumentDetails[] = result.typeDefinition ? this.parseArgsDefinition( result.typeDefinition ) : [];

const option: IOption<O, A> = {
const option: IOption = {
name: '',
description: desc,
args,
Expand Down Expand Up @@ -383,7 +383,7 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
option.aliases.push( name );
}

if ( this.cmd.getOption( name ) ) {
if ( this.cmd.getBaseOption( name, true ) ) {
if ( opts?.override ) {
this.removeOption( name );
} else {
Expand Down Expand Up @@ -598,7 +598,7 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
stopEarly,
knownFlaks,
allowEmpty: this._allowEmpty,
flags: this.options,
flags: this.getOptions( true ),
parse: ( type: string, option: IFlagOptions, arg: IFlagArgument, nextValue: string ) => {
const parser = this.types[ type ];
return parser instanceof Type ? parser.parse( option, arg, nextValue ) : parser( option, arg, nextValue );
Expand Down Expand Up @@ -698,7 +698,7 @@ export class BaseCommand<O = any, A extends Array<any> = any> {

if ( required.length ) {
const flagNames: string[] = Object.keys( flags );
const hasStandaloneOption: boolean = !!flagNames.find( name => this.getOption( name )?.standalone );
const hasStandaloneOption: boolean = !!flagNames.find( name => this.getOption( name, true )?.standalone );

if ( !hasStandaloneOption ) {
throw this.error( new Error( 'Missing argument(s): ' + required.join( ', ' ) ) );
Expand Down Expand Up @@ -801,13 +801,13 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
*
* @param flags Command options.
*/
protected findActionFlag( flags: O ): IOption<O, A> | undefined {
protected findActionFlag( flags: O ): IOption | undefined {

const flagNames = Object.keys( flags );

for ( const flag of flagNames ) {

const option = this.getOption( flag );
const option = this.getOption( flag, true );

if ( option?.action ) {
return option;
Expand Down Expand Up @@ -904,48 +904,114 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
/**
* Checks whether the command has options or not.
*/
public hasOptions( includeHidden?: boolean ): boolean {
public hasOptions( hidden?: boolean ): boolean {

if ( includeHidden ) {
return this.options.length > 0;
}
return this.getOptions( hidden ).length > 0;
}

public getOptions( hidden?: boolean ): IOption[] {

return this.options.filter( opt => !opt.hidden ).length > 0;
return this.getGlobalOptions( hidden ).concat( this.getBaseOptions( hidden ) );
}

public getOptions( includeHidden?: boolean ): IOption<O>[] {
public getBaseOptions( hidden?: boolean ): IOption[] {

if ( includeHidden ) {
return this.options;
if ( !this.options.length ) {
return [];
}

return this.options.filter( opt => !opt.hidden );
return hidden ? this.options.slice( 0 ) : this.options.filter( opt => !opt.hidden );
}

public getGlobalOptions( hidden?: boolean ): IOption[] {

const getOptions = ( cmd: BaseCommand | undefined, options: IOption[] = [], names: string[] = [] ): IOption[] => {

if ( cmd && cmd.options.length ) {

cmd.options.forEach( ( option: IOption ) => {
if (
option.global &&
!this.options.find( opt => opt.name === option.name ) &&
names.indexOf( option.name ) === -1 &&
( hidden || !option.hidden )
) {
names.push( option.name );
options.push( option );
}
} );

return getOptions( cmd._parent, options, names );
}

return options;
};

return getOptions( this._parent );
}

/**
* Checks whether the command has an option with given name or not.
*
* @param name Name of the option. Must be in param-case.
* @param hidden Include hidden options.
*/
public hasOption( name: string ): boolean {
public hasOption( name: string, hidden?: boolean ): boolean {

return !!this.getOption( name );
return !!this.getOption( name, hidden );
}

/**
* Get option by name.
*
* @param name Name of the option. Must be in param-case.
* @param hidden Include hidden options.
*/
public getOption( name: string ): IOption<O> | undefined {
public getOption( name: string, hidden?: boolean ): IOption | undefined {

return this.getBaseOption( name, hidden ) ?? this.getGlobalOption( name, hidden );
}

/**
* Get base option by name.
*
* @param name Name of the option. Must be in param-case.
* @param hidden Include hidden options.
*/
public getBaseOption( name: string, hidden?: boolean ): IOption | undefined {

const option = this.options.find( option => option.name === name );

return option && ( hidden || !option.hidden ) ? option : undefined;
}

/**
* Get global option from parent command's by name.
*
* @param name Name of the option. Must be in param-case.
* @param hidden Include hidden options.
*/
public getGlobalOption( name: string, hidden?: boolean ): IOption | undefined {

if ( !this._parent ) {
return;
}

let option: IOption | undefined = this._parent.getBaseOption( name, hidden );

if ( !option || !option.global ) {
return this._parent.getGlobalOption( name, hidden );
}

return this.options.find( option => option.name === name );
return option;
}

/**
* Remove option by name.
*
* @param name Name of the option. Must be in param-case.
*/
public removeOption( name: string ): IOption<O> | undefined {
public removeOption( name: string ): IOption | undefined {

const index = this.options.findIndex( option => option.name === name );

Expand All @@ -959,23 +1025,23 @@ export class BaseCommand<O = any, A extends Array<any> = any> {
/**
* Checks whether the command has sub-commands or not.
*/
public hasCommands( includeHidden?: boolean ): boolean {
public hasCommands( hidden?: boolean ): boolean {

if ( includeHidden ) {
if ( hidden ) {
return this.commands.size > 0;
}

return this.getCommands( includeHidden ).length > 0;
return this.getCommands( hidden ).length > 0;
}

/**
* Get sub-commands.
*/
public getCommands( includeHidden?: boolean ): BaseCommand[] {
public getCommands( hidden?: boolean ): BaseCommand[] {

const cmds: BaseCommand[] = Array.from( this.commands.values() );

if ( includeHidden ) {
if ( hidden ) {
return cmds;
}

Expand Down
5 changes: 3 additions & 2 deletions packages/command/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface IArgumentDetails extends IFlagArgument {
}

/** Command settings. */
export interface ICommandOption<O, A extends Array<any>> extends Omit<Omit<Omit<Omit<Omit<Omit<Omit<IFlagOptions,
export interface ICommandOption<O = any, A extends Array<any> = any> extends Omit<Omit<Omit<Omit<Omit<Omit<Omit<IFlagOptions,
'name'>,
'args'>,
'type'>,
Expand All @@ -28,6 +28,7 @@ export interface ICommandOption<O, A extends Array<any>> extends Omit<Omit<Omit<
'list'> {
override?: boolean;
hidden?: boolean;
global?: boolean;
action?: IAction<O, A>;
}

Expand All @@ -54,7 +55,7 @@ export interface IExample {
}

/** Result of `cmd.parse()`. */
export interface IParseResult<O, A> {
export interface IParseResult<O = any, A extends Array<any> = any> {
options: O,
args: A
cmd: BaseCommand<O>;
Expand Down
57 changes: 57 additions & 0 deletions packages/command/test/option/global_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { IFlagArgument, IFlagOptions } from '../../../flags/lib/types.ts';
import { Command } from '../../lib/command.ts';
import { assertEquals } from '../lib/assert.ts';

const cmd = new Command()
.version( '0.1.0' )
.option( '-b, --base', 'Only available on this command.' )
.option( '-g, --global [val:custom]', 'Available on all command\'s.', { global: true } )
.type( 'custom', (option: IFlagOptions, arg: IFlagArgument, value: string) => value.toUpperCase() )
.command( 'sub-command', new Command()
.option( '-l, --level2 [val:custom]', 'Only available on this command.' )
.description( 'Some sub command.' )
.command( 'sub-command', new Command()
.option( '-L, --level3 [val:custom]', 'Only available on this command.' )
.description( 'Some nested sub command.' )
)
);

Deno.test( 'command with global option', async () => {

const { options, args } = await cmd.parse( [ '-g', 'halo' ] );

assertEquals( options, { global: 'HALO' } );
assertEquals( args, [] );
} );

Deno.test( 'sub command with global option', async () => {

const { options, args } = await cmd.parse( [ 'sub-command', '-g', 'halo' ] );

assertEquals( options, { global: 'HALO' } );
assertEquals( args, [] );
} );

Deno.test( 'nested sub command with global option', async () => {

const { options, args } = await cmd.parse( [ 'sub-command', 'sub-command', '-g', 'halo' ] );

assertEquals( options, { global: 'HALO' } );
assertEquals( args, [] );
} );

Deno.test( 'sub command with global option', async () => {

const { options, args } = await cmd.parse( [ 'sub-command', '-l', 'halo' ] );

assertEquals( options, { level2: 'HALO' } );
assertEquals( args, [] );
} );

Deno.test( 'nested sub command with global option', async () => {

const { options, args } = await cmd.parse( [ 'sub-command', 'sub-command', '-L', 'halo' ] );

assertEquals( options, { level3: 'HALO' } );
assertEquals( args, [] );
} );

0 comments on commit 7d6e7cf

Please sign in to comment.