Skip to content

Commit

Permalink
Add isMultiple option for flags (#143)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
ulken and sindresorhus committed May 5, 2020
1 parent d9d42a2 commit c4c5ee2
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 20 deletions.
5 changes: 4 additions & 1 deletion index.d.ts
Expand Up @@ -7,6 +7,7 @@ declare namespace meow {
readonly type?: Type;
readonly alias?: string;
readonly default?: Default;
readonly isMultiple?: boolean;
}

type StringFlag = Flag<'string', string>;
Expand All @@ -24,14 +25,16 @@ declare namespace meow {
- `type`: Type of value. (Possible values: `string` `boolean` `number`)
- `alias`: Usually used to define a short flag alias.
- `default`: Default value when the flag is not specified.
- `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false)
@example
```
flags: {
unicorn: {
type: 'string',
alias: 'u',
default: 'rainbow'
default: ['rainbow', 'cat'],
isMultiple: true
}
}
```
Expand Down
69 changes: 53 additions & 16 deletions index.js
Expand Up @@ -9,11 +9,51 @@ const redent = require('redent');
const readPkgUp = require('read-pkg-up');
const hardRejection = require('hard-rejection');
const normalizePackageData = require('normalize-package-data');
const arrify = require('arrify');

// Prevent caching of this module so module.parent is always accurate
delete require.cache[__filename];
const parentDir = path.dirname(module.parent.filename);

const buildParserFlags = ({flags, booleanDefault}) =>
Object.entries(flags).reduce((parserFlags, [flagKey, flagValue]) => {
const flag = {...flagValue};

if (
typeof booleanDefault !== 'undefined' &&
flag.type === 'boolean' &&
!Object.prototype.hasOwnProperty.call(flag, 'default')
) {
flag.default = flag.isMultiple ? [booleanDefault] : booleanDefault;
}

if (flag.isMultiple) {
flag.type = 'array';
delete flag.isMultiple;
}

parserFlags[flagKey] = flag;

return parserFlags;
}, {});

/**
Convert to alternative syntax for coercing values to expected type, according to https://github.com/yargs/yargs-parser#requireyargs-parserargs-opts.
*/
const convertToTypedArrayOption = (arrayOption, flags) =>
arrify(arrayOption).map(flagKey => ({
key: flagKey,
[flags[flagKey].type || 'string']: true
}));

const validateFlags = (flags, options) => {
for (const [flagKey, flagValue] of Object.entries(options.flags)) {
if (flagKey !== '--' && !flagValue.isMultiple && Array.isArray(flags[flagKey])) {
throw new Error(`The flag --${flagKey} can only be set once.`);
}
}
};

const meow = (helpText, options) => {
if (typeof helpText !== 'string') {
options = helpText;
Expand All @@ -26,6 +66,7 @@ const meow = (helpText, options) => {
normalize: false
}).packageJson || {},
argv: process.argv.slice(2),
flags: {},
inferType: false,
input: 'string',
help: helpText,
Expand All @@ -40,20 +81,9 @@ const meow = (helpText, options) => {
hardRejection();
}

const parserFlags = options.flags && typeof options.booleanDefault !== 'undefined' ? Object.keys(options.flags).reduce(
(flags, flag) => {
if (flags[flag].type === 'boolean' && !Object.prototype.hasOwnProperty.call(flags[flag], 'default')) {
flags[flag].default = options.booleanDefault;
}

return flags;
},
options.flags
) : options.flags;

let parserOptions = {
arguments: options.input,
...parserFlags
...buildParserFlags(options)
};

parserOptions = decamelizeKeys(parserOptions, '-', {exclude: ['stopEarly', '--']});
Expand All @@ -71,6 +101,13 @@ const meow = (helpText, options) => {
};
}

if (parserOptions.array !== undefined) {
// `yargs` supports 'string|number|boolean' arrays,
// but `minimist-options` only support 'string' as element type.
// Open issue to add support to `minimist-options`: https://github.com/vadimdemedes/minimist-options/issues/18.
parserOptions.array = convertToTypedArrayOption(parserOptions.array, options.flags);
}

const {pkg} = options;
const argv = yargs(options.argv, parserOptions);
let help = redent(trimNewlines((options.help || '').replace(/\t+\n*$/, '')), 2);
Expand Down Expand Up @@ -112,10 +149,10 @@ const meow = (helpText, options) => {
const flags = camelcaseKeys(argv, {exclude: ['--', /^\w$/]});
const unnormalizedFlags = {...flags};

if (options.flags !== undefined) {
for (const flagValue of Object.values(options.flags)) {
delete flags[flagValue.alias];
}
validateFlags(flags, options);

for (const flagValue of Object.values(options.flags)) {
delete flags[flagValue.alias];
}

return {
Expand Down
4 changes: 2 additions & 2 deletions index.test-d.ts
Expand Up @@ -35,8 +35,8 @@ const result = meow('Help text', {
foo: {type: 'boolean', alias: 'f'},
'foo-bar': {type: 'number'},
bar: {type: 'string', default: ''}
}}
);
}
});

expectType<string[]>(result.input);
expectType<PackageJson>(result.pkg);
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -41,6 +41,7 @@
],
"dependencies": {
"@types/minimist": "^1.2.0",
"arrify": "^2.0.1",
"camelcase-keys": "^6.2.2",
"decamelize-keys": "^1.1.0",
"hard-rejection": "^2.1.0",
Expand Down
4 changes: 3 additions & 1 deletion readme.md
Expand Up @@ -137,6 +137,7 @@ The key is the flag name and the value is an object with any of:
- `type`: Type of value. (Possible values: `string` `boolean` `number`)
- `alias`: Usually used to define a short flag alias.
- `default`: Default value when the flag is not specified.
- `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false)
Example:
Expand All @@ -145,7 +146,8 @@ flags: {
unicorn: {
type: 'string',
alias: 'u',
default: 'rainbow'
default: ['rainbow', 'cat'],
isMultiple: true
}
}
```
Expand Down
170 changes: 170 additions & 0 deletions test.js
Expand Up @@ -311,6 +311,176 @@ test('supports `number` flag type - throws on incorrect default value', t => {
});
});

test('isMultiple - flag set once returns array', t => {
t.deepEqual(meow({
argv: ['--foo=bar'],
flags: {
foo: {
type: 'string',
isMultiple: true
}
}
}).flags, {
foo: ['bar']
});
});

test('isMultiple - flag set multiple times', t => {
t.deepEqual(meow({
argv: ['--foo=bar', '--foo=baz'],
flags: {
foo: {
type: 'string',
isMultiple: true
}
}
}).flags, {
foo: ['bar', 'baz']
});
});

test('isMultiple - flag with space separated values', t => {
t.deepEqual(meow({
argv: ['--foo', 'bar', 'baz'],
flags: {
foo: {
type: 'string',
isMultiple: true
}
}
}).flags, {
foo: ['bar', 'baz']
});
});

test('single flag set more than once => throws', t => {
t.throws(() => {
meow({
argv: ['--foo=bar', '--foo=baz'],
flags: {
foo: {
type: 'string'
}
}
});
}, {message: 'The flag --foo can only be set once.'});
});

test('isMultiple - boolean flag', t => {
t.deepEqual(meow({
argv: ['--foo', '--foo=false'],
flags: {
foo: {
type: 'boolean',
isMultiple: true
}
}
}).flags, {
foo: [true, false]
});
});

test('isMultiple - boolean flag is false by default', t => {
t.deepEqual(meow({
argv: [],
flags: {
foo: {
type: 'boolean',
isMultiple: true
}
}
}).flags, {
foo: [false]
});
});

test('isMultiple - flag with `booleanDefault: undefined` => filter out unset boolean args', t => {
t.deepEqual(meow({
argv: ['--foo'],
booleanDefault: undefined,
flags: {
foo: {
type: 'boolean',
isMultiple: true
},
bar: {
type: 'boolean',
isMultiple: true
}
}
}).flags, {
foo: [true]
});
});

test('isMultiple - number flag', t => {
t.deepEqual(meow({
argv: ['--foo=1.3', '--foo=-1'],
flags: {
foo: {
type: 'number',
isMultiple: true
}
}
}).flags, {
foo: [1.3, -1]
});
});

test('isMultiple - flag default values', t => {
t.deepEqual(meow({
argv: [],
flags: {
string: {
type: 'string',
isMultiple: true,
default: ['foo']
},
boolean: {
type: 'boolean',
isMultiple: true,
default: [true]
},
number: {
type: 'number',
isMultiple: true,
default: [0.5]
}
}
}).flags, {
string: ['foo'],
boolean: [true],
number: [0.5]
});
});

test('isMultiple - multiple flag default values', t => {
t.deepEqual(meow({
argv: [],
flags: {
string: {
type: 'string',
isMultiple: true,
default: ['foo', 'bar']
},
boolean: {
type: 'boolean',
isMultiple: true,
default: [true, false]
},
number: {
type: 'number',
isMultiple: true,
default: [0.5, 1]
}
}
}).flags, {
string: ['foo', 'bar'],
boolean: [true, false],
number: [0.5, 1]
});
});

if (NODE_MAJOR_VERSION >= 14) {
test('supports es modules', async t => {
try {
Expand Down

0 comments on commit c4c5ee2

Please sign in to comment.