Skip to content

Commit 1eede6a

Browse files
sbencodingsindresorhus
andauthoredMay 7, 2020
Add isRequired flag option (#141)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent c4c5ee2 commit 1eede6a

10 files changed

+304
-15
lines changed
 

‎index.d.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,21 @@ import {PackageJson} from 'type-fest';
33
declare namespace meow {
44
type FlagType = 'string' | 'boolean' | 'number';
55

6+
/**
7+
Callback function to determine if a flag is required during runtime.
8+
9+
@param flags - Contains the flags converted to camel-case excluding aliases.
10+
@param input - Contains the non-flag arguments.
11+
12+
@returns True if the flag is required, otherwise false.
13+
*/
14+
type IsRequiredPredicate = (flags: Readonly<AnyFlags>, input: readonly string[]) => boolean;
15+
616
interface Flag<Type extends FlagType, Default> {
717
readonly type?: Type;
818
readonly alias?: string;
919
readonly default?: Default;
20+
readonly isRequired?: boolean | IsRequiredPredicate;
1021
readonly isMultiple?: boolean;
1122
}
1223

@@ -25,6 +36,8 @@ declare namespace meow {
2536
- `type`: Type of value. (Possible values: `string` `boolean` `number`)
2637
- `alias`: Usually used to define a short flag alias.
2738
- `default`: Default value when the flag is not specified.
39+
- `isRequired`: Determine if the flag is required.
40+
If it's only known at runtime whether the flag is requried or not you can pass a Function instead of a boolean, which based on the given flags and other non-flag arguments should decide if the flag is required.
2841
- `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false)
2942
3043
@example
@@ -34,7 +47,14 @@ declare namespace meow {
3447
type: 'string',
3548
alias: 'u',
3649
default: ['rainbow', 'cat'],
37-
isMultiple: true
50+
isMultiple: true,
51+
isRequired: (flags, input) => {
52+
if (flags.otherFlag) {
53+
return true;
54+
}
55+
56+
return false;
57+
}
3858
}
3959
}
4060
```

‎index.js

+49
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,46 @@ const arrify = require('arrify');
1515
delete require.cache[__filename];
1616
const parentDir = path.dirname(module.parent.filename);
1717

18+
const isFlagMissing = (flagName, definedFlags, receivedFlags, input) => {
19+
const flag = definedFlags[flagName];
20+
let isFlagRequired = true;
21+
22+
if (typeof flag.isRequired === 'function') {
23+
isFlagRequired = flag.isRequired(receivedFlags, input);
24+
if (typeof isFlagRequired !== 'boolean') {
25+
throw new TypeError(`Return value for isRequired callback should be of type boolean, but ${typeof isFlagRequired} was returned.`);
26+
}
27+
}
28+
29+
if (typeof receivedFlags[flagName] === 'undefined') {
30+
return isFlagRequired;
31+
}
32+
33+
return flag.isMultiple && receivedFlags[flagName].length === 0;
34+
};
35+
36+
const getMissingRequiredFlags = (flags, receivedFlags, input) => {
37+
const missingRequiredFlags = [];
38+
if (typeof flags === 'undefined') {
39+
return [];
40+
}
41+
42+
for (const flagName of Object.keys(flags)) {
43+
if (flags[flagName].isRequired && isFlagMissing(flagName, flags, receivedFlags, input)) {
44+
missingRequiredFlags.push({key: flagName, ...flags[flagName]});
45+
}
46+
}
47+
48+
return missingRequiredFlags;
49+
};
50+
51+
const reportMissingRequiredFlags = missingRequiredFlags => {
52+
console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`);
53+
for (const flag of missingRequiredFlags) {
54+
console.error(`\t--${flag.key}${flag.alias ? `, -${flag.alias}` : ''}`);
55+
}
56+
};
57+
1858
const buildParserFlags = ({flags, booleanDefault}) =>
1959
Object.entries(flags).reduce((parserFlags, [flagKey, flagValue]) => {
2060
const flag = {...flagValue};
@@ -155,6 +195,15 @@ const meow = (helpText, options) => {
155195
delete flags[flagValue.alias];
156196
}
157197

198+
// Get a list of missing flags that are required
199+
const missingRequiredFlags = getMissingRequiredFlags(options.flags, flags, input);
200+
201+
// Print error message for missing flags that are required
202+
if (missingRequiredFlags.length > 0) {
203+
reportMissingRequiredFlags(missingRequiredFlags);
204+
process.exit(2);
205+
}
206+
158207
return {
159208
input,
160209
flags,

‎package.json

+5
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,10 @@
6868
"ignores": [
6969
"estest/index.js"
7070
]
71+
},
72+
"ava": {
73+
"files": [
74+
"test/*"
75+
]
7176
}
7277
}

‎readme.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ 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+
- `isRequired`: Determine if the flag is required. (Default: false)
141+
- If it's only known at runtime whether the flag is requried or not, you can pass a `Function` instead of a `boolean`, which based on the given flags and other non-flag arguments, should decide if the flag is required. Two arguments are passed to the function:
142+
- The first argument is the **flags** object, which contains the flags converted to camel-case excluding aliases.
143+
- The second argument is the **input** string array, which contains the non-flag arguments.
144+
- The function should return a `boolean`, true if the flag is required, otherwise false.
140145
- `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false)
141146
142147
Example:
@@ -147,7 +152,14 @@ flags: {
147152
type: 'string',
148153
alias: 'u',
149154
default: ['rainbow', 'cat'],
150-
isMultiple: true
155+
isMultiple: true,
156+
isRequired: (flags, input) => {
157+
if (flags.otherFlag) {
158+
return true;
159+
}
160+
161+
return false;
162+
}
151163
}
152164
}
153165
```
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
const meow = require('../..');
4+
5+
const cli = meow({
6+
description: 'Custom description',
7+
help: `
8+
Usage
9+
foo <input>
10+
`,
11+
flags: {
12+
trigger: {
13+
type: 'boolean',
14+
alias: 't'
15+
},
16+
withTrigger: {
17+
type: 'string',
18+
isRequired: (flags, _) => {
19+
return flags.trigger;
20+
}
21+
},
22+
allowError: {
23+
type: 'boolean',
24+
alias: 'a'
25+
},
26+
shouldError: {
27+
type: 'boolean',
28+
isRequired: (flags, _) => {
29+
if (flags.allowError) {
30+
return 'should error';
31+
}
32+
33+
return false;
34+
}
35+
}
36+
}
37+
});
38+
39+
console.log(`${cli.flags.trigger},${cli.flags.withTrigger}`);
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
const meow = require('../..');
4+
5+
const cli = meow({
6+
description: 'Custom description',
7+
help: `
8+
Usage
9+
foo <input>
10+
`,
11+
flags: {
12+
test: {
13+
type: 'number',
14+
alias: 't',
15+
isRequired: true,
16+
isMultiple: true
17+
}
18+
}
19+
});
20+
21+
console.log(cli.flags.test);

‎test/fixtures/fixture-required.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
const meow = require('../..');
4+
5+
const cli = meow({
6+
description: 'Custom description',
7+
help: `
8+
Usage
9+
foo <input>
10+
`,
11+
flags: {
12+
test: {
13+
type: 'string',
14+
alias: 't',
15+
isRequired: true
16+
},
17+
number: {
18+
type: 'number',
19+
isRequired: true
20+
},
21+
notRequired: {
22+
type: 'string'
23+
}
24+
}
25+
});
26+
27+
console.log(`${cli.flags.test},${cli.flags.number}`);

‎fixture.js ‎test/fixtures/fixture.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
'use strict';
3-
const meow = require('.');
3+
const meow = require('../..');
44

55
const cli = meow({
66
description: 'Custom description',

‎test/is-required-flag.js

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import test from 'ava';
2+
import execa from 'execa';
3+
const path = require('path');
4+
5+
const fixtureRequiredPath = path.join(__dirname, 'fixtures', 'fixture-required.js');
6+
const fixtureRequiredFunctionPath = path.join(__dirname, 'fixtures', 'fixture-required-function.js');
7+
const fixtureRequiredMultiplePath = path.join(__dirname, 'fixtures', 'fixture-required-multiple.js');
8+
9+
test('spawn cli and test not specifying required flags', async t => {
10+
try {
11+
await execa(fixtureRequiredPath, []);
12+
} catch (error) {
13+
const {stderr, message} = error;
14+
t.regex(message, /Command failed with exit code 2/);
15+
t.regex(stderr, /Missing required flag/);
16+
t.regex(stderr, /--test, -t/);
17+
t.regex(stderr, /--number/);
18+
t.notRegex(stderr, /--notRequired/);
19+
}
20+
});
21+
22+
test('spawn cli and test specifying all required flags', async t => {
23+
const {stdout} = await execa(fixtureRequiredPath, [
24+
'-t',
25+
'test',
26+
'--number',
27+
'6'
28+
]);
29+
t.is(stdout, 'test,6');
30+
});
31+
32+
test('spawn cli and test specifying required string flag with an empty string as value', async t => {
33+
try {
34+
await execa(fixtureRequiredPath, ['--test', '']);
35+
} catch (error) {
36+
const {stderr, message} = error;
37+
t.regex(message, /Command failed with exit code 2/);
38+
t.regex(stderr, /Missing required flag/);
39+
t.notRegex(stderr, /--test, -t/);
40+
}
41+
});
42+
43+
test('spawn cli and test specifying required number flag without a number', async t => {
44+
try {
45+
await execa(fixtureRequiredPath, ['--number']);
46+
} catch (error) {
47+
const {stderr, message} = error;
48+
t.regex(message, /Command failed with exit code 2/);
49+
t.regex(stderr, /Missing required flag/);
50+
t.regex(stderr, /--number/);
51+
}
52+
});
53+
54+
test('spawn cli and test setting isRequired as a function and not specifying any flags', async t => {
55+
const {stdout} = await execa(fixtureRequiredFunctionPath, []);
56+
t.is(stdout, 'false,undefined');
57+
});
58+
59+
test('spawn cli and test setting isRequired as a function and specifying only the flag that activates the isRequired condition for the other flag', async t => {
60+
try {
61+
await execa(fixtureRequiredFunctionPath, ['--trigger']);
62+
} catch (error) {
63+
const {stderr, message} = error;
64+
t.regex(message, /Command failed with exit code 2/);
65+
t.regex(stderr, /Missing required flag/);
66+
t.regex(stderr, /--withTrigger/);
67+
}
68+
});
69+
70+
test('spawn cli and test setting isRequired as a function and specifying both the flags', async t => {
71+
const {stdout} = await execa(fixtureRequiredFunctionPath, ['--trigger', '--withTrigger', 'specified']);
72+
t.is(stdout, 'true,specified');
73+
});
74+
75+
test('spawn cli and test setting isRequired as a function and check if returning a non-boolean value throws an error', async t => {
76+
try {
77+
await execa(fixtureRequiredFunctionPath, ['--allowError', '--shouldError', 'specified']);
78+
} catch (error) {
79+
const {stderr, message} = error;
80+
t.regex(message, /Command failed with exit code 1/);
81+
t.regex(stderr, /Return value for isRequired callback should be of type boolean, but string was returned./);
82+
}
83+
});
84+
85+
test('spawn cli and test isRequired with isMultiple giving a single value', async t => {
86+
const {stdout} = await execa(fixtureRequiredMultiplePath, ['--test', '1']);
87+
t.is(stdout, '[ 1 ]');
88+
});
89+
90+
test('spawn cli and test isRequired with isMultiple giving a multiple values', async t => {
91+
const {stdout} = await execa(fixtureRequiredMultiplePath, ['--test', '1', '2', '3']);
92+
t.is(stdout, '[ 1, 2, 3 ]');
93+
});
94+
95+
test('spawn cli and test isRequired with isMultiple giving no values, but flag is given', async t => {
96+
try {
97+
await execa(fixtureRequiredMultiplePath, ['--test']);
98+
} catch (error) {
99+
const {stderr, message} = error;
100+
t.regex(message, /Command failed with exit code 2/);
101+
t.regex(stderr, /Missing required flag/);
102+
t.regex(stderr, /--test/);
103+
}
104+
});
105+
106+
test('spawn cli and test isRequired with isMultiple giving no values, but flag is not given', async t => {
107+
try {
108+
await execa(fixtureRequiredMultiplePath, []);
109+
} catch (error) {
110+
const {stderr, message} = error;
111+
t.regex(message, /Command failed with exit code 2/);
112+
t.regex(stderr, /Missing required flag/);
113+
t.regex(stderr, /--test/);
114+
}
115+
});

‎test.js ‎test/test.js

+13-12
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import test from 'ava';
22
import indentString from 'indent-string';
33
import execa from 'execa';
44
import path from 'path';
5-
import pkg from './package.json';
6-
import meow from '.';
5+
import pkg from '../package.json';
6+
import meow from '..';
77

8+
const fixturePath = path.join(__dirname, 'fixtures', 'fixture.js');
89
const NODE_MAJOR_VERSION = process.versions.node.split('.')[0];
910

1011
test('return object', t => {
@@ -39,47 +40,47 @@ test('support help shortcut', t => {
3940
});
4041

4142
test('spawn cli and show version', async t => {
42-
const {stdout} = await execa('./fixture.js', ['--version']);
43+
const {stdout} = await execa(fixturePath, ['--version']);
4344
t.is(stdout, pkg.version);
4445
});
4546

4647
test('spawn cli and disabled autoVersion and autoHelp', async t => {
47-
const {stdout} = await execa('./fixture.js', ['--version', '--help']);
48+
const {stdout} = await execa(fixturePath, ['--version', '--help']);
4849
t.is(stdout, 'version\nhelp\nmeow\ncamelCaseOption');
4950
});
5051

5152
test('spawn cli and disabled autoVersion', async t => {
52-
const {stdout} = await execa('./fixture.js', ['--version', '--no-auto-version']);
53+
const {stdout} = await execa(fixturePath, ['--version', '--no-auto-version']);
5354
t.is(stdout, 'version\nautoVersion\nmeow\ncamelCaseOption');
5455
});
5556

5657
test('spawn cli and not show version', async t => {
57-
const {stdout} = await execa('./fixture.js', ['--version=beta']);
58+
const {stdout} = await execa(fixturePath, ['--version=beta']);
5859
t.is(stdout, 'version\nmeow\ncamelCaseOption');
5960
});
6061

6162
test('spawn cli and show help screen', async t => {
62-
const {stdout} = await execa('./fixture.js', ['--help']);
63+
const {stdout} = await execa(fixturePath, ['--help']);
6364
t.is(stdout, indentString('\nCustom description\n\nUsage\n foo <input>\n\n', 2));
6465
});
6566

6667
test('spawn cli and disabled autoHelp', async t => {
67-
const {stdout} = await execa('./fixture.js', ['--help', '--no-auto-help']);
68+
const {stdout} = await execa(fixturePath, ['--help', '--no-auto-help']);
6869
t.is(stdout, 'help\nautoHelp\nmeow\ncamelCaseOption');
6970
});
7071

7172
test('spawn cli and not show help', async t => {
72-
const {stdout} = await execa('./fixture.js', ['--help=all']);
73+
const {stdout} = await execa(fixturePath, ['--help=all']);
7374
t.is(stdout, 'help\nmeow\ncamelCaseOption');
7475
});
7576

7677
test('spawn cli and test input', async t => {
77-
const {stdout} = await execa('./fixture.js', ['-u', 'cat']);
78+
const {stdout} = await execa(fixturePath, ['-u', 'cat']);
7879
t.is(stdout, 'unicorn\nmeow\ncamelCaseOption');
7980
});
8081

8182
test('spawn cli and test input flag', async t => {
82-
const {stdout} = await execa('./fixture.js', ['--camel-case-option', 'bar']);
83+
const {stdout} = await execa(fixturePath, ['--camel-case-option', 'bar']);
8384
t.is(stdout, 'bar');
8485
});
8586

@@ -485,7 +486,7 @@ if (NODE_MAJOR_VERSION >= 14) {
485486
test('supports es modules', async t => {
486487
try {
487488
const {stdout} = await execa('node', ['index.js', '--version'], {
488-
cwd: path.join(__dirname, 'estest')
489+
cwd: path.join(__dirname, '..', 'estest')
489490
});
490491
t.regex(stdout, /1.2.3/);
491492
} catch (error) {

0 commit comments

Comments
 (0)
Please sign in to comment.