Skip to content

Commit

Permalink
breaking(flags): remove optional argument from boolean flags which wa…
Browse files Browse the repository at this point in the history
…s registered per default (#40)

A boolean flag no longer has an optional value per default. To add an optional or required value use the `optionalValue` or `requiredValue` option.
  • Loading branch information
c4spar committed Jun 11, 2020
1 parent ae371d9 commit 00ac846
Show file tree
Hide file tree
Showing 11 changed files with 196 additions and 89 deletions.
51 changes: 26 additions & 25 deletions packages/flags/README.md
Expand Up @@ -72,37 +72,38 @@ if ( flags.help ) {

### parseFlags Options

| Param | Type | Required | Description |
| ----- | :--: | :--: | ----------- |
| allowEmpty | `boolean` | No | Allow no arguments. Defaults to `false` |
| stopEarly | `boolean` | No | If enabled, all values starting from the first non option argument will be added to `unknown`. |
| flags | `IFlagOptions[]` | No | Array of flag options. |
| parse | `function` | No | Custom type parser. |
| Param | Type | Required | Description |
| ---------- |:----------------:|:--------:| ---------------------------------------------------------------------------------------------- |
| allowEmpty | `boolean` | No | Allow no arguments. Defaults to `false` |
| stopEarly | `boolean` | No | If enabled, all values starting from the first non option argument will be added to `unknown`. |
| flags | `IFlagOptions[]` | No | Array of flag options. |
| parse | `function` | No | Custom type parser. |

### Flag Options

| Param | Type | Required | Description |
| ----- | :--: | :--: | ----------- |
| name | `string` | Yes | The name of the option. |
| args | `IFlagArgument[]` | No | An Array of argument options. |
| aliases | `string[]` | No | Array of option alias's. |
| standalone | `boolean ` | No | Cannot be combined with other options. |
| default | `any` | No | Default option value. |
| required | `boolean ` | No | Mark option as required and throw an error if the option is missing. |
| depends | `string[]` | No | Array of option names that depends on this option. |
| conflicts | `string[]` | No | Array of option names that conflicts with this option. |
| collect | `boolean` | No | Allow to call this option multiple times and add each value to an array which will be returned as result. |
| value | `( val: any, previous?: any ) => any` | No | Custom value processing. |
| Param | Type | Required | Description |
| ---------- |:-------------------------------------:|:--------:| --------------------------------------------------------------------------------------------------------- |
| name | `string` | Yes | The name of the option. |
| args | `IFlagArgument[]` | No | An Array of argument options. |
| aliases | `string[]` | No | Array of option alias's. |
| standalone | `boolean ` | No | Cannot be combined with other options. |
| default | `any` | No | Default option value. |
| required | `boolean ` | No | Mark option as required and throw an error if the option is missing. |
| depends | `string[]` | No | Array of option names that depends on this option. |
| conflicts | `string[]` | No | Array of option names that conflicts with this option. |
| collect | `boolean` | No | Allow to call this option multiple times and add each value to an array which will be returned as result. |
| value | `( val: any, previous?: any ) => any` | No | Custom value processing. |

### Argument options

| Param | Type | Required | Description |
| ----- | :--: | :--: | ----------- |
| type | `OptionType \| string` | no | Type of the argument. |
| optionalValue | `boolean` | no | Make argument optional. |
| variadic | `boolean` | no | Make arguments variadic. |
| list | `boolean` | no | Split argument by `separator`. |
| separator | `string` | no | List separator. Defaults to `,` |
| Param | Type | Required | Description |
| ------------- |:----------------------:|:--------:| ------------------------------- |
| type | `OptionType \| string` | no | Type of the argument. |
| optionalValue | `boolean` | no | Make argument optional. |
| requiredValue | `boolean` | no | Make argument required. |
| variadic | `boolean` | no | Make arguments variadic. |
| list | `boolean` | no | Split argument by `separator`. |
| separator | `string` | no | List separator. Defaults to `,` |

## Custom type processing

Expand Down
106 changes: 65 additions & 41 deletions packages/flags/lib/flags.ts
Expand Up @@ -23,7 +23,6 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
!opts.flags && ( opts.flags = [] );

const normalized = normalize( args );
let option: IFlagOptions | undefined;

let inLiteral = false;
let negate = false;
Expand All @@ -48,6 +47,8 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):

for ( let i = 0; i < normalized.length; i++ ) {

let option: IFlagOptions | undefined;
let args: IFlagArgument[] | undefined;
const current = normalized[ i ];

// literal args after --
Expand Down Expand Up @@ -89,10 +90,6 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
};
}

if ( !option.args || !option.args.length ) {
option.args = [ option ];
}

if ( !option.name ) {
throw new Error( `Missing name for option: ${ current }` );
}
Expand All @@ -103,19 +100,29 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
throw new Error( `Duplicate option: ${ current }` );
}

args = option.args?.length ? option.args : [ {
type: option.type,
requiredValue: option.requiredValue,
optionalValue: option.optionalValue,
variadic: option.variadic,
list: option.list,
separator: option.separator
} ];

let argIndex = 0;
let inOptionalArg = false;
const previous = flags[ friendlyName ];

parseNext();
parseNext( option, args );

if ( typeof flags[ friendlyName ] === 'undefined' ) {

if ( typeof option.default !== 'undefined' ) {
flags[ friendlyName ] = typeof option.default === 'function' ? option.default() : option.default;
} else if ( option.args && option.args[ 0 ].optionalValue ) {
flags[ friendlyName ] = true;
} else {
} else if ( args[ argIndex ].requiredValue ) {
throw new Error( `Missing value for option: --${ option.name }` );
} else {
flags[ friendlyName ] = true;
}
}

Expand All @@ -127,17 +134,43 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
flags[ friendlyName ] = value;
}

function parseNext(): void {
/** Parse next argument for current option. */
function parseNext( option: IFlagOptions, args: IFlagArgument[] ): void {

if ( !option ) {
throw new Error( 'Wrongly used parseNext.' );
}
const arg: IFlagArgument = args[ argIndex ];

if ( !option.args || !option.args[ argIndex ] ) {
if ( !arg ) {
throw new Error( 'Unknown option: ' + next() );
}

let arg: IFlagArgument = option.args[ argIndex ];
if ( !arg.type ) {
arg.type = OptionType.BOOLEAN;
}

if ( option.args?.length ) {
// make all value's required per default
if ( ( typeof arg.optionalValue === 'undefined' || arg.optionalValue === false ) &&
typeof arg.requiredValue === 'undefined'
) {
arg.requiredValue = true;
}
} else {
// make non boolean value required per default
if ( arg.type !== OptionType.BOOLEAN &&
( typeof arg.optionalValue === 'undefined' || arg.optionalValue === false ) &&
typeof arg.requiredValue === 'undefined'
) {
arg.requiredValue = true;
}
}

if ( arg.requiredValue ) {
if ( inOptionalArg ) {
throw new Error( `An required argument can not follow an optional argument but found in: ${ option.name }` );
}
} else {
inOptionalArg = true;
}

if ( negate ) {
if ( arg.type !== OptionType.BOOLEAN && !arg.optionalValue ) {
Expand All @@ -147,22 +180,17 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
return;
}

// make boolean value optional per default
if ( option.type === OptionType.BOOLEAN && typeof option.optionalValue === 'undefined' ) {
option.optionalValue = true;
}

let result: IFlagValue | undefined;
let increase = false;

if ( arg.list && hasNext() ) {
if ( arg.list && hasNext( arg ) ) {

const parsed: IFlagValueType[] = next()
.split( arg.separator || ',' )
.map( ( nextValue: string ) => {
const value = parseValue( nextValue );
const value = parseValue( option, arg, nextValue );
if ( typeof value === 'undefined' ) {
throw new Error( `List item of option --${ option?.name } must be of type ${ option?.type } but got: ${ nextValue }` );
throw new Error( `List item of option --${ option?.name } must be of type ${ arg.type } but got: ${ nextValue }` );
}
return value;
} );
Expand All @@ -171,8 +199,8 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
result = parsed;
}
} else {
if ( hasNext() ) {
result = parseValue( next() );
if ( hasNext( arg ) ) {
result = parseValue( option, arg, next() );
} else if ( arg.optionalValue && arg.type === OptionType.BOOLEAN ) {
result = true;
}
Expand All @@ -182,12 +210,12 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
i++;
if ( !arg.variadic ) {
argIndex++;
} else if ( option.args && option.args[ argIndex + 1 ] ) {
} else if ( args[ argIndex + 1 ] ) {
throw new Error( 'An argument cannot follow an variadic argument: ' + next() );
}
}

if ( typeof result !== 'undefined' && ( ( option.args && option.args.length > 1 ) || arg.variadic ) ) {
if ( typeof result !== 'undefined' && ( ( args.length > 1 ) || arg.variadic ) ) {

if ( !flags[ friendlyName ] ) {
flags[ friendlyName ] = [];
Expand All @@ -196,31 +224,27 @@ export function parseFlags<O = any>( args: string[], opts: IParseOptions = {} ):
( flags[ friendlyName ] as IFlagValue[] ).push( result );


if ( hasNext() ) {
parseNext();
if ( hasNext( arg ) ) {
parseNext( option, args );
}
} else {
flags[ friendlyName ] = result;
}

function hasNext(): boolean {

return typeof normalized[ i + 1 ] !== 'undefined' &&
/** Check if current option should have an argument. */
function hasNext( arg: IFlagArgument ): boolean {

return !!(
normalized[ i + 1 ] &&
( arg.optionalValue || arg.requiredValue || arg.variadic ) &&
( normalized[ i + 1 ][ 0 ] !== '-' ||
( arg.type === OptionType.NUMBER && !isNaN( normalized[ i + 1 ] as any ) )
) &&

// ( arg.type !== OptionType.BOOLEAN || [ 'true', 'false', '1', '0' ].indexOf( normalized[ i + 1 ] ) !== -1 ) &&

typeof arg !== 'undefined';
arg );
}

function parseValue( nextValue: string ): IFlagValueType {

if ( !option ) {
throw new Error( 'Wrongly used parseValue.' );
}
/** Parse argument value. */
function parseValue( option: IFlagOptions, arg: IFlagArgument, nextValue: string ): IFlagValueType {

let result: IFlagValueType = opts.parse ?
opts.parse( arg.type || OptionType.STRING, option, arg, nextValue ) :
Expand Down
1 change: 1 addition & 0 deletions packages/flags/lib/types.ts
Expand Up @@ -40,6 +40,7 @@ export interface IFlagsResult<O = any> {
export interface IFlagArgument {
type?: OptionType | string;
optionalValue?: boolean;
requiredValue?: boolean;
variadic?: boolean;
list?: boolean;
separator?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/flags/lib/validate-flags.ts
Expand Up @@ -77,7 +77,7 @@ export function validateFlags( flags: IFlagOptions[], values: IFlags, knownFlaks

option.args?.forEach( ( arg: IFlagArgument, i: number ) => {

if ( !arg.optionalValue
if ( arg.requiredValue
&& (
typeof values[ name ] === 'undefined'
|| ( isArray && typeof ( values[ name ] as IFlagValue[] )[ i ] === 'undefined' )
Expand Down
1 change: 1 addition & 0 deletions packages/flags/test/option/collect_test.ts
Expand Up @@ -18,6 +18,7 @@ const options = <IParseOptions>{
name: 'boolean',
aliases: [ 'b' ],
type: OptionType.BOOLEAN,
optionalValue: true,
collect: true
}, {
name: 'number',
Expand Down
2 changes: 2 additions & 0 deletions packages/flags/test/option/default_test.ts
Expand Up @@ -8,6 +8,7 @@ const options = <IParseOptions>{
name: 'boolean',
aliases: [ 'b' ],
type: OptionType.BOOLEAN,
optionalValue: true,
default: false
}, {
name: 'string',
Expand All @@ -23,6 +24,7 @@ const options = <IParseOptions>{
name: 'boolean2',
aliases: [ 'B' ],
type: OptionType.BOOLEAN,
optionalValue: true,
default: true
}, {
name: 'string2',
Expand Down
3 changes: 3 additions & 0 deletions packages/flags/test/option/depends_test.ts
Expand Up @@ -99,14 +99,17 @@ const options2 = {
flags: [ {
name: 'standalone',
type: OptionType.BOOLEAN,
optionalValue: true,
standalone: true
}, {
name: 'flag1',
type: OptionType.BOOLEAN,
optionalValue: true,
depends: [ 'flag2' ]
}, {
name: 'flag2',
type: OptionType.BOOLEAN,
optionalValue: true,
depends: [ 'flag1' ],
default: false
} ]
Expand Down
19 changes: 14 additions & 5 deletions packages/flags/test/option/value_test.ts
Expand Up @@ -3,25 +3,34 @@ import { IParseOptions } from '../../lib/types.ts';
import { assertEquals } from '../lib/assert.ts';

const options = <IParseOptions>{
stopEarly: false,
allowEmpty: false,
flags: [ {
name: 'flag',
aliases: [ 'f' ],
optionalValue: true,
value( value: string ): string[] {
return [ value ];
}
}, {
name: 'flag2',
aliases: [ 'F' ]
aliases: [ 'F' ],
optionalValue: true
} ]
};

Deno.test( 'flags optionVariadic optional', () => {
Deno.test( 'flags: value handler', () => {

const { flags, unknown, literal } = parseFlags( [ '-f', '-F' ], options );

assertEquals( flags, { flag: [ true ], flag2: true } );
assertEquals( unknown, [] );
assertEquals( literal, [] );
} );

Deno.test( 'flags: value handler with optional arg', () => {

const { flags, unknown, literal } = parseFlags( [ '-f', '1', '-F', '1' ], options );

assertEquals( flags, { flag: [ '1' ], flag2: '1' } );
assertEquals( flags, { flag: [ true ], flag2: true } );
assertEquals( unknown, [] );
assertEquals( literal, [] );
} );

0 comments on commit 00ac846

Please sign in to comment.