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
Changes from 13 commits
abd6b12
3d2f6c8
9b0c22a
45fb69a
aebcf85
6bde170
c095990
1d9fa7d
1f6423a
a5c24a2
bf0d816
8b03f9f
854e294
e25cfdc
c54437d
80cc740
019dd32
940fb96
3c1c710
22f0a77
e11a0ad
e5dac8b
1e04bdc
59de8b8
3dee237
4b7a7ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,11 +2,13 @@ import {PackageJson} from 'type-fest'; | |
|
||
declare namespace meow { | ||
type FlagType = 'string' | 'boolean' | 'number'; | ||
type IsRequiredPredicate = (flags: AnyFlags, input: string[]) => boolean; | ||
|
||
interface Flag<Type extends FlagType, Default> { | ||
readonly type?: Type; | ||
readonly alias?: string; | ||
readonly default?: Default; | ||
readonly isRequired?: boolean | IsRequiredPredicate | ||
} | ||
|
||
type StringFlag = Flag<'string', string>; | ||
|
@@ -24,6 +26,11 @@ 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`: Boolean or Function that specifies if this flag is required. | ||
Two arguments are passed to the function. | ||
The first arguments is the flags object, it contains the flags converted to camelCase excluding aliases. | ||
The second arugment is the input string array, it contains the non-flag arguments. | ||
The function should return a Boolean, true if the flag is requried, otherwise false. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The function docs should be at https://github.com/sindresorhus/meow/pull/141/files#diff-b52768974e6bc0faccb7d4b75b162c99R5 and you don't have to mention the types. It's already shown by the actual types. Mention what it does and what problem it solves. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I was just trying to make sure that the |
||
|
||
@example | ||
``` | ||
|
@@ -32,6 +39,11 @@ declare namespace meow { | |
type: 'string', | ||
alias: 'u', | ||
default: 'rainbow' | ||
isRequired: (flags, input) => { | ||
if (flags.otherFlag) { | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,37 @@ const normalizePackageData = require('normalize-package-data'); | |
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); | ||
} | ||
|
||
return typeof receivedFlags[flagName] === 'undefined' && isFlagRequired; | ||
}; | ||
|
||
const getMissingRequiredFlags = (flags, receivedFlags, input) => { | ||
const missingRequiredFlags = []; | ||
if (typeof flags !== 'undefined') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use an early-return here to reduce the nesting. |
||
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 meow = (helpText, options) => { | ||
if (typeof helpText !== 'string') { | ||
options = helpText; | ||
|
@@ -118,6 +149,15 @@ const meow = (helpText, options) => { | |
} | ||
} | ||
|
||
// 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, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,5 +63,10 @@ | |
"rules": { | ||
"unicorn/no-process-exit": "off" | ||
} | ||
}, | ||
"ava": { | ||
"files": [ | ||
"test/*" | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,6 +97,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`: Boolean or Function that specifies if this flag is required. | ||
Two arguments are passed to the function. | ||
The first arguments is the **flags** object, it contains the flags converted to camelCase excluding aliases. | ||
The second arugment is the **input** string array, it contains the non-flag arguments. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo |
||
The function should return a Boolean, true if the flag is requried, otherwise false. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo |
||
|
||
Example: | ||
|
||
|
@@ -106,6 +111,11 @@ flags: { | |
type: 'string', | ||
alias: 'u', | ||
default: 'rainbow' | ||
isRequired: (flags, input) => { | ||
if (flags.otherFlag) { | ||
return true; | ||
} | ||
sindresorhus marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
``` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
#!/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; | ||
} | ||
} | ||
} | ||
}); | ||
console.log(`${cli.flags.trigger},${cli.flags.withTrigger}`); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
#!/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}`); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import test from 'ava'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The filename can just be |
||
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'); | ||
|
||
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'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.