Skip to content

Commit c4c5ee2

Browse files
ulkensindresorhus
andauthoredMay 5, 2020
Add isMultiple option for flags (#143)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent d9d42a2 commit c4c5ee2

File tree

6 files changed

+233
-20
lines changed

6 files changed

+233
-20
lines changed
 

‎index.d.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ declare namespace meow {
77
readonly type?: Type;
88
readonly alias?: string;
99
readonly default?: Default;
10+
readonly isMultiple?: boolean;
1011
}
1112

1213
type StringFlag = Flag<'string', string>;
@@ -24,14 +25,16 @@ declare namespace meow {
2425
- `type`: Type of value. (Possible values: `string` `boolean` `number`)
2526
- `alias`: Usually used to define a short flag alias.
2627
- `default`: Default value when the flag is not specified.
28+
- `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false)
2729
2830
@example
2931
```
3032
flags: {
3133
unicorn: {
3234
type: 'string',
3335
alias: 'u',
34-
default: 'rainbow'
36+
default: ['rainbow', 'cat'],
37+
isMultiple: true
3538
}
3639
}
3740
```

‎index.js

+53-16
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,51 @@ const redent = require('redent');
99
const readPkgUp = require('read-pkg-up');
1010
const hardRejection = require('hard-rejection');
1111
const normalizePackageData = require('normalize-package-data');
12+
const arrify = require('arrify');
1213

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

18+
const buildParserFlags = ({flags, booleanDefault}) =>
19+
Object.entries(flags).reduce((parserFlags, [flagKey, flagValue]) => {
20+
const flag = {...flagValue};
21+
22+
if (
23+
typeof booleanDefault !== 'undefined' &&
24+
flag.type === 'boolean' &&
25+
!Object.prototype.hasOwnProperty.call(flag, 'default')
26+
) {
27+
flag.default = flag.isMultiple ? [booleanDefault] : booleanDefault;
28+
}
29+
30+
if (flag.isMultiple) {
31+
flag.type = 'array';
32+
delete flag.isMultiple;
33+
}
34+
35+
parserFlags[flagKey] = flag;
36+
37+
return parserFlags;
38+
}, {});
39+
40+
/**
41+
Convert to alternative syntax for coercing values to expected type, according to https://github.com/yargs/yargs-parser#requireyargs-parserargs-opts.
42+
*/
43+
const convertToTypedArrayOption = (arrayOption, flags) =>
44+
arrify(arrayOption).map(flagKey => ({
45+
key: flagKey,
46+
[flags[flagKey].type || 'string']: true
47+
}));
48+
49+
const validateFlags = (flags, options) => {
50+
for (const [flagKey, flagValue] of Object.entries(options.flags)) {
51+
if (flagKey !== '--' && !flagValue.isMultiple && Array.isArray(flags[flagKey])) {
52+
throw new Error(`The flag --${flagKey} can only be set once.`);
53+
}
54+
}
55+
};
56+
1757
const meow = (helpText, options) => {
1858
if (typeof helpText !== 'string') {
1959
options = helpText;
@@ -26,6 +66,7 @@ const meow = (helpText, options) => {
2666
normalize: false
2767
}).packageJson || {},
2868
argv: process.argv.slice(2),
69+
flags: {},
2970
inferType: false,
3071
input: 'string',
3172
help: helpText,
@@ -40,20 +81,9 @@ const meow = (helpText, options) => {
4081
hardRejection();
4182
}
4283

43-
const parserFlags = options.flags && typeof options.booleanDefault !== 'undefined' ? Object.keys(options.flags).reduce(
44-
(flags, flag) => {
45-
if (flags[flag].type === 'boolean' && !Object.prototype.hasOwnProperty.call(flags[flag], 'default')) {
46-
flags[flag].default = options.booleanDefault;
47-
}
48-
49-
return flags;
50-
},
51-
options.flags
52-
) : options.flags;
53-
5484
let parserOptions = {
5585
arguments: options.input,
56-
...parserFlags
86+
...buildParserFlags(options)
5787
};
5888

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

104+
if (parserOptions.array !== undefined) {
105+
// `yargs` supports 'string|number|boolean' arrays,
106+
// but `minimist-options` only support 'string' as element type.
107+
// Open issue to add support to `minimist-options`: https://github.com/vadimdemedes/minimist-options/issues/18.
108+
parserOptions.array = convertToTypedArrayOption(parserOptions.array, options.flags);
109+
}
110+
74111
const {pkg} = options;
75112
const argv = yargs(options.argv, parserOptions);
76113
let help = redent(trimNewlines((options.help || '').replace(/\t+\n*$/, '')), 2);
@@ -112,10 +149,10 @@ const meow = (helpText, options) => {
112149
const flags = camelcaseKeys(argv, {exclude: ['--', /^\w$/]});
113150
const unnormalizedFlags = {...flags};
114151

115-
if (options.flags !== undefined) {
116-
for (const flagValue of Object.values(options.flags)) {
117-
delete flags[flagValue.alias];
118-
}
152+
validateFlags(flags, options);
153+
154+
for (const flagValue of Object.values(options.flags)) {
155+
delete flags[flagValue.alias];
119156
}
120157

121158
return {

‎index.test-d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ const result = meow('Help text', {
3535
foo: {type: 'boolean', alias: 'f'},
3636
'foo-bar': {type: 'number'},
3737
bar: {type: 'string', default: ''}
38-
}}
39-
);
38+
}
39+
});
4040

4141
expectType<string[]>(result.input);
4242
expectType<PackageJson>(result.pkg);

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
],
4242
"dependencies": {
4343
"@types/minimist": "^1.2.0",
44+
"arrify": "^2.0.1",
4445
"camelcase-keys": "^6.2.2",
4546
"decamelize-keys": "^1.1.0",
4647
"hard-rejection": "^2.1.0",

‎readme.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ The key is the flag name and the value is an object with any of:
137137
- `type`: Type of value. (Possible values: `string` `boolean` `number`)
138138
- `alias`: Usually used to define a short flag alias.
139139
- `default`: Default value when the flag is not specified.
140+
- `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false)
140141
141142
Example:
142143
@@ -145,7 +146,8 @@ flags: {
145146
unicorn: {
146147
type: 'string',
147148
alias: 'u',
148-
default: 'rainbow'
149+
default: ['rainbow', 'cat'],
150+
isMultiple: true
149151
}
150152
}
151153
```

‎test.js

+170
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,176 @@ test('supports `number` flag type - throws on incorrect default value', t => {
311311
});
312312
});
313313

314+
test('isMultiple - flag set once returns array', t => {
315+
t.deepEqual(meow({
316+
argv: ['--foo=bar'],
317+
flags: {
318+
foo: {
319+
type: 'string',
320+
isMultiple: true
321+
}
322+
}
323+
}).flags, {
324+
foo: ['bar']
325+
});
326+
});
327+
328+
test('isMultiple - flag set multiple times', t => {
329+
t.deepEqual(meow({
330+
argv: ['--foo=bar', '--foo=baz'],
331+
flags: {
332+
foo: {
333+
type: 'string',
334+
isMultiple: true
335+
}
336+
}
337+
}).flags, {
338+
foo: ['bar', 'baz']
339+
});
340+
});
341+
342+
test('isMultiple - flag with space separated values', t => {
343+
t.deepEqual(meow({
344+
argv: ['--foo', 'bar', 'baz'],
345+
flags: {
346+
foo: {
347+
type: 'string',
348+
isMultiple: true
349+
}
350+
}
351+
}).flags, {
352+
foo: ['bar', 'baz']
353+
});
354+
});
355+
356+
test('single flag set more than once => throws', t => {
357+
t.throws(() => {
358+
meow({
359+
argv: ['--foo=bar', '--foo=baz'],
360+
flags: {
361+
foo: {
362+
type: 'string'
363+
}
364+
}
365+
});
366+
}, {message: 'The flag --foo can only be set once.'});
367+
});
368+
369+
test('isMultiple - boolean flag', t => {
370+
t.deepEqual(meow({
371+
argv: ['--foo', '--foo=false'],
372+
flags: {
373+
foo: {
374+
type: 'boolean',
375+
isMultiple: true
376+
}
377+
}
378+
}).flags, {
379+
foo: [true, false]
380+
});
381+
});
382+
383+
test('isMultiple - boolean flag is false by default', t => {
384+
t.deepEqual(meow({
385+
argv: [],
386+
flags: {
387+
foo: {
388+
type: 'boolean',
389+
isMultiple: true
390+
}
391+
}
392+
}).flags, {
393+
foo: [false]
394+
});
395+
});
396+
397+
test('isMultiple - flag with `booleanDefault: undefined` => filter out unset boolean args', t => {
398+
t.deepEqual(meow({
399+
argv: ['--foo'],
400+
booleanDefault: undefined,
401+
flags: {
402+
foo: {
403+
type: 'boolean',
404+
isMultiple: true
405+
},
406+
bar: {
407+
type: 'boolean',
408+
isMultiple: true
409+
}
410+
}
411+
}).flags, {
412+
foo: [true]
413+
});
414+
});
415+
416+
test('isMultiple - number flag', t => {
417+
t.deepEqual(meow({
418+
argv: ['--foo=1.3', '--foo=-1'],
419+
flags: {
420+
foo: {
421+
type: 'number',
422+
isMultiple: true
423+
}
424+
}
425+
}).flags, {
426+
foo: [1.3, -1]
427+
});
428+
});
429+
430+
test('isMultiple - flag default values', t => {
431+
t.deepEqual(meow({
432+
argv: [],
433+
flags: {
434+
string: {
435+
type: 'string',
436+
isMultiple: true,
437+
default: ['foo']
438+
},
439+
boolean: {
440+
type: 'boolean',
441+
isMultiple: true,
442+
default: [true]
443+
},
444+
number: {
445+
type: 'number',
446+
isMultiple: true,
447+
default: [0.5]
448+
}
449+
}
450+
}).flags, {
451+
string: ['foo'],
452+
boolean: [true],
453+
number: [0.5]
454+
});
455+
});
456+
457+
test('isMultiple - multiple flag default values', t => {
458+
t.deepEqual(meow({
459+
argv: [],
460+
flags: {
461+
string: {
462+
type: 'string',
463+
isMultiple: true,
464+
default: ['foo', 'bar']
465+
},
466+
boolean: {
467+
type: 'boolean',
468+
isMultiple: true,
469+
default: [true, false]
470+
},
471+
number: {
472+
type: 'number',
473+
isMultiple: true,
474+
default: [0.5, 1]
475+
}
476+
}
477+
}).flags, {
478+
string: ['foo', 'bar'],
479+
boolean: [true, false],
480+
number: [0.5, 1]
481+
});
482+
});
483+
314484
if (NODE_MAJOR_VERSION >= 14) {
315485
test('supports es modules', async t => {
316486
try {

0 commit comments

Comments
 (0)
Please sign in to comment.