Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add isMultiple option for flags #143

Merged
merged 20 commits into from May 5, 2020
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.1.1",
"decamelize-keys": "^1.1.0",
"hard-rejection": "^2.0.0",
Expand Down
4 changes: 3 additions & 1 deletion readme.md
Expand Up @@ -97,6 +97,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 @@ -105,7 +106,8 @@ flags: {
unicorn: {
type: 'string',
alias: 'u',
default: 'rainbow'
default: ['rainbow', 'cat'],
isMultiple: true
}
}
```
Expand Down
143 changes: 143 additions & 0 deletions test.js
Expand Up @@ -307,3 +307,146 @@ 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 => {
ulken marked this conversation as resolved.
Show resolved Hide resolved
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]
});
});