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 isRequired flag option #141

Merged
merged 26 commits into from May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
abd6b12
Add isRequired option for flags
sbencoding Mar 24, 2020
3d2f6c8
Add ability to specify isRequired as a function
sbencoding Mar 24, 2020
9b0c22a
Add type definition for isRequired
sbencoding Mar 24, 2020
45fb69a
Sync readme with typedef comments and examples
sbencoding Mar 24, 2020
aebcf85
Merge feature branch with upstream/master
sbencoding Apr 28, 2020
6bde170
Use console.error, better variable naming
sbencoding Apr 28, 2020
c095990
Update documentation
sbencoding Apr 28, 2020
1d9fa7d
Use stderr, because missing flags are printed with console.error
sbencoding Apr 28, 2020
1f6423a
Split out isRequired logic to separate methods
sbencoding Apr 28, 2020
a5c24a2
Organize tests and fixtures, update affected paths
sbencoding Apr 28, 2020
bf0d816
Split out isRequired flag related tests to separate file, better test…
sbencoding Apr 28, 2020
8b03f9f
Specify fixture path better
sbencoding Apr 28, 2020
854e294
Add isRequired (as function) return value to documentation
sbencoding Apr 28, 2020
e25cfdc
Merge branch 'master' into feature-flag-is-required
sbencoding May 6, 2020
c54437d
Fix broken state after merge
sbencoding May 6, 2020
80cc740
Improve documentation
sbencoding May 6, 2020
019dd32
Simplify code, check isRequired callback return value type
sbencoding May 6, 2020
940fb96
Add test for isRequired callback return value validation
sbencoding May 6, 2020
3c1c710
Add handling of isMultiple flags to the isRequired logic
sbencoding May 6, 2020
22f0a77
Add tests for isRequired and isMultiple options set at the same time
sbencoding May 6, 2020
e11a0ad
Fix estest path
sbencoding May 6, 2020
e5dac8b
Update index.d.ts
sindresorhus May 7, 2020
1e04bdc
Update readme.md
sindresorhus May 7, 2020
59de8b8
Update fixture-required-function.js
sindresorhus May 7, 2020
3dee237
Update fixture-required-multiple.js
sindresorhus May 7, 2020
4b7a7ac
Update fixture-required.js
sindresorhus May 7, 2020
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
22 changes: 21 additions & 1 deletion index.d.ts
Expand Up @@ -3,10 +3,21 @@ import {PackageJson} from 'type-fest';
declare namespace meow {
type FlagType = 'string' | 'boolean' | 'number';

/**
Callback function to determine if a flag is required during runtime.

@param flags - Contains the flags converted to camel-case excluding aliases.
@param input - Contains the non-flag arguments.

@returns True if the flag is required, otherwise false.
*/
type IsRequiredPredicate = (flags: Readonly<AnyFlags>, input: readonly string[]) => boolean;

interface Flag<Type extends FlagType, Default> {
readonly type?: Type;
readonly alias?: string;
readonly default?: Default;
readonly isRequired?: boolean | IsRequiredPredicate;
readonly isMultiple?: boolean;
}

Expand All @@ -25,6 +36,8 @@ 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.
- `isRequired`: Determine if the flag is required.
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.
- `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false)

@example
Expand All @@ -34,7 +47,14 @@ declare namespace meow {
type: 'string',
alias: 'u',
default: ['rainbow', 'cat'],
isMultiple: true
isMultiple: true,
isRequired: (flags, input) => {
if (flags.otherFlag) {
return true;
}

return false;
}
}
}
```
Expand Down
49 changes: 49 additions & 0 deletions index.js
Expand Up @@ -15,6 +15,46 @@ const arrify = require('arrify');
delete require.cache[__filename];
const parentDir = path.dirname(module.parent.filename);

const isFlagMissing = (flagName, definedFlags, receivedFlags, input) => {
const flag = definedFlags[flagName];
let isFlagRequired = true;

if (typeof flag.isRequired === 'function') {
isFlagRequired = flag.isRequired(receivedFlags, input);
if (typeof isFlagRequired !== 'boolean') {
throw new TypeError(`Return value for isRequired callback should be of type boolean, but ${typeof isFlagRequired} was returned.`);
}
}

if (typeof receivedFlags[flagName] === 'undefined') {
return isFlagRequired;
}

return flag.isMultiple && receivedFlags[flagName].length === 0;
};

const getMissingRequiredFlags = (flags, receivedFlags, input) => {
const missingRequiredFlags = [];
if (typeof flags === 'undefined') {
return [];
}

for (const flagName of Object.keys(flags)) {
if (flags[flagName].isRequired && isFlagMissing(flagName, flags, receivedFlags, input)) {
missingRequiredFlags.push({key: flagName, ...flags[flagName]});
}
}

return missingRequiredFlags;
};

const reportMissingRequiredFlags = missingRequiredFlags => {
console.error(`Missing required flag${missingRequiredFlags.length > 1 ? 's' : ''}`);
for (const flag of missingRequiredFlags) {
console.error(`\t--${flag.key}${flag.alias ? `, -${flag.alias}` : ''}`);
}
};

const buildParserFlags = ({flags, booleanDefault}) =>
Object.entries(flags).reduce((parserFlags, [flagKey, flagValue]) => {
const flag = {...flagValue};
Expand Down Expand Up @@ -155,6 +195,15 @@ const meow = (helpText, options) => {
delete flags[flagValue.alias];
}

// Get a list of missing flags that are required
const missingRequiredFlags = getMissingRequiredFlags(options.flags, flags, input);

// Print error message for missing flags that are required
if (missingRequiredFlags.length > 0) {
reportMissingRequiredFlags(missingRequiredFlags);
process.exit(2);
}

sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
return {
input,
flags,
Expand Down
5 changes: 5 additions & 0 deletions package.json
Expand Up @@ -68,5 +68,10 @@
"ignores": [
"estest/index.js"
]
},
"ava": {
"files": [
"test/*"
]
}
}
14 changes: 13 additions & 1 deletion readme.md
Expand Up @@ -137,6 +137,11 @@ 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.
- `isRequired`: Determine if the flag is required. (Default: false)
- 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:
- The first argument is the **flags** object, which contains the flags converted to camel-case excluding aliases.
- The second argument is the **input** string array, which contains the non-flag arguments.
- The function should return a `boolean`, true if the flag is required, otherwise false.
- `isMultiple`: Indicates a flag can be set multiple times. Values are turned into an array. (Default: false)

Example:
Expand All @@ -147,7 +152,14 @@ flags: {
type: 'string',
alias: 'u',
default: ['rainbow', 'cat'],
isMultiple: true
isMultiple: true,
isRequired: (flags, input) => {
if (flags.otherFlag) {
return true;
}
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved

return false;
}
}
}
```
Expand Down
39 changes: 39 additions & 0 deletions test/fixtures/fixture-required-function.js
@@ -0,0 +1,39 @@
#!/usr/bin/env node
'use strict';
const meow = require('../..');

const cli = meow({
description: 'Custom description',
help: `
Usage
foo <input>
`,
flags: {
trigger: {
type: 'boolean',
alias: 't'
},
withTrigger: {
type: 'string',
isRequired: (flags, _) => {
return flags.trigger;
}
},
allowError: {
type: 'boolean',
alias: 'a'
},
shouldError: {
type: 'boolean',
isRequired: (flags, _) => {
if (flags.allowError) {
return 'should error';
}

return false;
}
}
}
});

console.log(`${cli.flags.trigger},${cli.flags.withTrigger}`);
21 changes: 21 additions & 0 deletions test/fixtures/fixture-required-multiple.js
@@ -0,0 +1,21 @@
#!/usr/bin/env node
'use strict';
const meow = require('../..');

const cli = meow({
description: 'Custom description',
help: `
Usage
foo <input>
`,
flags: {
test: {
type: 'number',
alias: 't',
isRequired: true,
isMultiple: true
}
}
});

console.log(cli.flags.test);
27 changes: 27 additions & 0 deletions test/fixtures/fixture-required.js
@@ -0,0 +1,27 @@
#!/usr/bin/env node
'use strict';
const meow = require('../..');

const cli = meow({
description: 'Custom description',
help: `
Usage
foo <input>
`,
flags: {
test: {
type: 'string',
alias: 't',
isRequired: true
},
number: {
type: 'number',
isRequired: true
},
notRequired: {
type: 'string'
}
}
});

console.log(`${cli.flags.test},${cli.flags.number}`);
2 changes: 1 addition & 1 deletion fixture.js → test/fixtures/fixture.js
@@ -1,6 +1,6 @@
#!/usr/bin/env node
'use strict';
const meow = require('.');
const meow = require('../..');

const cli = meow({
description: 'Custom description',
Expand Down
115 changes: 115 additions & 0 deletions test/is-required-flag.js
@@ -0,0 +1,115 @@
import test from 'ava';
import execa from 'execa';
const path = require('path');

const fixtureRequiredPath = path.join(__dirname, 'fixtures', 'fixture-required.js');
const fixtureRequiredFunctionPath = path.join(__dirname, 'fixtures', 'fixture-required-function.js');
const fixtureRequiredMultiplePath = path.join(__dirname, 'fixtures', 'fixture-required-multiple.js');

test('spawn cli and test not specifying required flags', async t => {
try {
await execa(fixtureRequiredPath, []);
} catch (error) {
const {stderr, message} = error;
t.regex(message, /Command failed with exit code 2/);
t.regex(stderr, /Missing required flag/);
t.regex(stderr, /--test, -t/);
t.regex(stderr, /--number/);
t.notRegex(stderr, /--notRequired/);
}
});

test('spawn cli and test specifying all required flags', async t => {
const {stdout} = await execa(fixtureRequiredPath, [
'-t',
'test',
'--number',
'6'
]);
t.is(stdout, 'test,6');
});

test('spawn cli and test specifying required string flag with an empty string as value', async t => {
try {
await execa(fixtureRequiredPath, ['--test', '']);
} catch (error) {
const {stderr, message} = error;
t.regex(message, /Command failed with exit code 2/);
t.regex(stderr, /Missing required flag/);
t.notRegex(stderr, /--test, -t/);
}
});

test('spawn cli and test specifying required number flag without a number', async t => {
try {
await execa(fixtureRequiredPath, ['--number']);
} catch (error) {
const {stderr, message} = error;
t.regex(message, /Command failed with exit code 2/);
t.regex(stderr, /Missing required flag/);
t.regex(stderr, /--number/);
}
});

test('spawn cli and test setting isRequired as a function and not specifying any flags', async t => {
const {stdout} = await execa(fixtureRequiredFunctionPath, []);
t.is(stdout, 'false,undefined');
});

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 => {
try {
await execa(fixtureRequiredFunctionPath, ['--trigger']);
} catch (error) {
const {stderr, message} = error;
t.regex(message, /Command failed with exit code 2/);
t.regex(stderr, /Missing required flag/);
t.regex(stderr, /--withTrigger/);
}
});

test('spawn cli and test setting isRequired as a function and specifying both the flags', async t => {
const {stdout} = await execa(fixtureRequiredFunctionPath, ['--trigger', '--withTrigger', 'specified']);
t.is(stdout, 'true,specified');
});

test('spawn cli and test setting isRequired as a function and check if returning a non-boolean value throws an error', async t => {
try {
await execa(fixtureRequiredFunctionPath, ['--allowError', '--shouldError', 'specified']);
} catch (error) {
const {stderr, message} = error;
t.regex(message, /Command failed with exit code 1/);
t.regex(stderr, /Return value for isRequired callback should be of type boolean, but string was returned./);
}
});

test('spawn cli and test isRequired with isMultiple giving a single value', async t => {
const {stdout} = await execa(fixtureRequiredMultiplePath, ['--test', '1']);
t.is(stdout, '[ 1 ]');
});

test('spawn cli and test isRequired with isMultiple giving a multiple values', async t => {
const {stdout} = await execa(fixtureRequiredMultiplePath, ['--test', '1', '2', '3']);
t.is(stdout, '[ 1, 2, 3 ]');
});

test('spawn cli and test isRequired with isMultiple giving no values, but flag is given', async t => {
try {
await execa(fixtureRequiredMultiplePath, ['--test']);
} catch (error) {
const {stderr, message} = error;
t.regex(message, /Command failed with exit code 2/);
t.regex(stderr, /Missing required flag/);
t.regex(stderr, /--test/);
}
});

test('spawn cli and test isRequired with isMultiple giving no values, but flag is not given', async t => {
try {
await execa(fixtureRequiredMultiplePath, []);
} catch (error) {
const {stderr, message} = error;
t.regex(message, /Command failed with exit code 2/);
t.regex(stderr, /Missing required flag/);
t.regex(stderr, /--test/);
}
});