-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
Add support to print help/usage of util.parseArgs #58875
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
base: main
Are you sure you want to change the base?
Changes from all commits
3530729
0156673
fc85ed2
8161bab
3caedd5
1b48504
4e3fb84
d91aec9
f1ae174
11e776f
aba7452
a565569
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 | ||||
---|---|---|---|---|---|---|
|
@@ -1921,6 +1921,10 @@ added: | |||||
- v18.3.0 | ||||||
- v16.17.0 | ||||||
changes: | ||||||
- version: | ||||||
- REPLACEME | ||||||
pr-url: https://github.com/nodejs/node/pull/58875 | ||||||
description: Add support for help text in options and general help text. | ||||||
- version: | ||||||
- v22.4.0 | ||||||
- v20.16.0 | ||||||
|
@@ -1959,6 +1963,7 @@ changes: | |||||
be used if (and only if) the option does not appear in the arguments to be | ||||||
parsed. It must be of the same type as the `type` property. When `multiple` | ||||||
is `true`, it must be an array. | ||||||
* `help` {string} Descriptive text to display in help output for this option. | ||||||
* `strict` {boolean} Should an error be thrown when unknown arguments | ||||||
are encountered, or when arguments are passed that do not match the | ||||||
`type` configured in `options`. | ||||||
|
@@ -1973,13 +1978,16 @@ changes: | |||||
the built-in behavior, from adding additional checks through to reprocessing | ||||||
the tokens in different ways. | ||||||
**Default:** `false`. | ||||||
* `help` {string} General help text to display at the beginning of help output. | ||||||
|
||||||
* Returns: {Object} The parsed command line arguments: | ||||||
* `values` {Object} A mapping of parsed option names with their {string} | ||||||
or {boolean} values. | ||||||
* `positionals` {string\[]} Positional arguments. | ||||||
* `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens) | ||||||
section. Only returned if `config` includes `tokens: true`. | ||||||
* `printUsage` {string | undefined} Formatted help text for all options provided. Only included if general `help` text | ||||||
is available. | ||||||
|
||||||
Provides a higher level API for command-line argument parsing than interacting | ||||||
with `process.argv` directly. Takes a specification for the expected arguments | ||||||
|
@@ -2025,6 +2033,86 @@ console.log(values, positionals); | |||||
// Prints: [Object: null prototype] { foo: true, bar: 'b' } [] | ||||||
``` | ||||||
|
||||||
### `parseArgs` help text | ||||||
|
||||||
`parseArgs` supports automatic formatted help text generation for command-line options. To use this feature, provide | ||||||
general help text using the `help` config property, and also | ||||||
add a `help` property to each option can be optionally included. | ||||||
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.
Suggested change
|
||||||
|
||||||
```mjs | ||||||
import { parseArgs } from 'node:util'; | ||||||
|
||||||
const options = { | ||||||
verbose: { | ||||||
type: 'boolean', | ||||||
short: 'v', | ||||||
}, | ||||||
help: { | ||||||
type: 'boolean', | ||||||
short: 'h', | ||||||
help: 'Prints command line options', | ||||||
}, | ||||||
output: { | ||||||
type: 'string', | ||||||
help: 'Output directory', | ||||||
}, | ||||||
}; | ||||||
|
||||||
// Get serialized help text in result | ||||||
const result = parseArgs({ | ||||||
options, | ||||||
help: 'My CLI Tool v1.0\n\nProcess files with various options.', | ||||||
}); | ||||||
|
||||||
if (result.printUsage) { | ||||||
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. With the current implementation, 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.
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.
Suggested change
|
||||||
console.log(result.printUsage); | ||||||
// Prints: | ||||||
// My CLI Tool v1.0 | ||||||
// | ||||||
// Process files with various options. | ||||||
// -v, --verbose | ||||||
// -h, --help. Prints command line options | ||||||
// --output <arg> Output directory | ||||||
} | ||||||
``` | ||||||
|
||||||
```cjs | ||||||
const { parseArgs } = require('node:util'); | ||||||
|
||||||
const options = { | ||||||
verbose: { | ||||||
type: 'boolean', | ||||||
short: 'v', | ||||||
}, | ||||||
help: { | ||||||
type: 'boolean', | ||||||
short: 'h', | ||||||
help: 'Prints command line options', | ||||||
}, | ||||||
output: { | ||||||
type: 'string', | ||||||
help: 'Output directory', | ||||||
}, | ||||||
}; | ||||||
|
||||||
// Get serialized help text in result | ||||||
const result = parseArgs({ | ||||||
options, | ||||||
help: 'My CLI Tool v1.0\n\nProcess files with various options.', | ||||||
}); | ||||||
|
||||||
if (result.printUsage) { | ||||||
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.
Suggested change
|
||||||
console.log(result.printUsage); | ||||||
// Prints: | ||||||
// My CLI Tool v1.0 | ||||||
// | ||||||
// Process files with various options. | ||||||
// -v, --verbose | ||||||
// -h, --help. Prints command line options | ||||||
// --output <arg> Output directory | ||||||
} | ||||||
``` | ||||||
|
||||||
### `parseArgs` `tokens` | ||||||
|
||||||
Detailed parse information is available for adding custom behaviors by | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -304,13 +304,51 @@ function argsToTokens(args, options) { | |
return tokens; | ||
} | ||
|
||
/** | ||
* Format help text for printing. | ||
* @param {string} longOption - long option name e.g. 'foo' | ||
* @param {object} optionConfig - option config from parseArgs({ options }) | ||
* @returns {string} formatted help text for printing | ||
* @example | ||
* formatHelpTextForPrint('foo', { type: 'string', help: 'help text' }) | ||
* // returns '--foo <arg> help text' | ||
*/ | ||
function formatHelpTextForPrint(longOption, optionConfig) { | ||
const layoutSpacing = 30; | ||
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. I found print has some logic related to layout spacing, maybe we can use something similar here. |
||
|
||
const shortOption = objectGetOwn(optionConfig, 'short'); | ||
const type = objectGetOwn(optionConfig, 'type'); | ||
const help = objectGetOwn(optionConfig, 'help'); | ||
|
||
let helpTextForPrint = ''; | ||
if (shortOption) { | ||
helpTextForPrint += `-${shortOption}, `; | ||
} | ||
helpTextForPrint += `--${longOption}`; | ||
if (type === 'string') { | ||
helpTextForPrint += ' <arg>'; | ||
} else if (type === 'boolean') { | ||
helpTextForPrint += ''; | ||
Comment on lines
+330
to
+331
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. I don't think these two lines do anything? |
||
} | ||
if (help) { | ||
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. I suggest that options without explicit help text are still included in the formatted help. This makes basic help simple. Add any help field, including describing the overall program, and you get formatted help returned with all the available options included. Yes, some users will want "hidden" options. But I think that is uncommon, and more useful to make it easy to get help text for simple programs with (well named) options, without needing to describe each option. 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. ( 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. It's also a lot easier to filter out help text you don't want, than to recreate and format help text you do want. 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. @shadowspawn @ljharb sounds good! I just pushed the changes related to this and removed the automatic printing, thanks for all the help so far! 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. I was imagining that supplying any I don't feel strongly about this. See what others think. If the help without general text was allowed it would look like:
|
||
if (helpTextForPrint.length > layoutSpacing) { | ||
helpTextForPrint += '\n' + ''.padEnd(layoutSpacing) + help; | ||
} else { | ||
helpTextForPrint = helpTextForPrint.padEnd(layoutSpacing) + help; | ||
} | ||
} | ||
|
||
return helpTextForPrint; | ||
} | ||
|
||
const parseArgs = (config = kEmptyObject) => { | ||
const args = objectGetOwn(config, 'args') ?? getMainArgs(); | ||
const strict = objectGetOwn(config, 'strict') ?? true; | ||
const allowPositionals = objectGetOwn(config, 'allowPositionals') ?? !strict; | ||
const returnTokens = objectGetOwn(config, 'tokens') ?? false; | ||
const allowNegative = objectGetOwn(config, 'allowNegative') ?? false; | ||
const options = objectGetOwn(config, 'options') ?? { __proto__: null }; | ||
const help = objectGetOwn(config, 'help') ?? ''; | ||
// Bundle these up for passing to strict-mode checks. | ||
const parseConfig = { args, strict, options, allowPositionals, allowNegative }; | ||
|
||
|
@@ -321,11 +359,11 @@ const parseArgs = (config = kEmptyObject) => { | |
validateBoolean(returnTokens, 'tokens'); | ||
validateBoolean(allowNegative, 'allowNegative'); | ||
validateObject(options, 'options'); | ||
validateString(help, 'help'); | ||
ArrayPrototypeForEach( | ||
ObjectEntries(options), | ||
({ 0: longOption, 1: optionConfig }) => { | ||
validateObject(optionConfig, `options.${longOption}`); | ||
|
||
// type is required | ||
const optionType = objectGetOwn(optionConfig, 'type'); | ||
validateUnion(optionType, `options.${longOption}.type`, ['string', 'boolean']); | ||
|
@@ -361,6 +399,11 @@ const parseArgs = (config = kEmptyObject) => { | |
} | ||
validator(defaultValue, `options.${longOption}.default`); | ||
} | ||
|
||
const helpOption = objectGetOwn(optionConfig, 'help'); | ||
if (ObjectHasOwn(optionConfig, 'help')) { | ||
validateString(helpOption, `options.${longOption}.help`); | ||
} | ||
}, | ||
); | ||
|
||
|
@@ -403,6 +446,23 @@ const parseArgs = (config = kEmptyObject) => { | |
} | ||
}); | ||
|
||
// Phase 4: generate print usage for each option | ||
let printUsage = ''; | ||
if (help) { | ||
printUsage += help; | ||
} | ||
ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { | ||
const helpTextForPrint = formatHelpTextForPrint(longOption, optionConfig); | ||
|
||
if (printUsage.length > 0) { | ||
printUsage += '\n'; | ||
} | ||
printUsage += helpTextForPrint; | ||
}); | ||
|
||
if (help && printUsage.length > 0) { | ||
result.printUsage = printUsage; | ||
} | ||
|
||
return result; | ||
}; | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -1062,3 +1062,131 @@ test('auto-detect --no-foo as negated when strict:false and allowNegative', () = | |||||
process.argv = holdArgv; | ||||||
process.execArgv = holdExecArgv; | ||||||
}); | ||||||
|
||||||
test('help arg value config must be a string', () => { | ||||||
const args = ['-f', 'bar']; | ||||||
const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; | ||||||
const help = true; | ||||||
assert.throws(() => { | ||||||
parseArgs({ args, options, help }); | ||||||
}, /The "help" argument must be of type string/ | ||||||
); | ||||||
}); | ||||||
|
||||||
test('help value for option must be a string', () => { | ||||||
const args = []; | ||||||
const options = { alpha: { type: 'string', help: true } }; | ||||||
assert.throws(() => { | ||||||
parseArgs({ args, options }); | ||||||
}, /"options\.alpha\.help" property must be of type string/ | ||||||
); | ||||||
}); | ||||||
|
||||||
test('when help arg with help value for lone short option is added, then add help text', () => { | ||||||
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 name of the test seems to be describing the arguments to be parsed rather than the interesting variations of the formatted help. I might be misunderstanding the intent. Perhaps:
Suggested change
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. Was I think things of interest to test are:
You are testing all of these (so good coverage), but the test names seem to be describing the supplied |
||||||
const args = ['-f', 'bar']; | ||||||
const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; | ||||||
const help = 'Description for some awesome stuff:'; | ||||||
const printUsage = help + '\n-f, --foo <arg> help text'; | ||||||
const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; | ||||||
const result = parseArgs({ args, options, allowPositionals: true, help }); | ||||||
assert.deepStrictEqual(result, expected); | ||||||
}); | ||||||
|
||||||
test('when help arg with help value for short group option is added, then add help text', () => { | ||||||
const args = ['-fm', 'bar']; | ||||||
const options = { foo: { type: 'boolean', short: 'f', help: 'help text' }, | ||||||
moo: { type: 'string', short: 'm', help: 'help text' } }; | ||||||
const help = 'Description for some awesome stuff:'; | ||||||
const printUsage = help + '\n-f, --foo help text\n-m, --moo <arg> help text'; | ||||||
const expected = { values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [], printUsage }; | ||||||
const result = parseArgs({ args, options, allowPositionals: true, help }); | ||||||
assert.deepStrictEqual(result, expected); | ||||||
}); | ||||||
|
||||||
test('when help arg with help value for short option and value is added, then add help text', () => { | ||||||
const args = ['-fFILE']; | ||||||
const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; | ||||||
const help = 'Description for some awesome stuff:'; | ||||||
const printUsage = help + '\n-f, --foo <arg> help text'; | ||||||
const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [], printUsage }; | ||||||
const result = parseArgs({ args, options, allowPositionals: true, help }); | ||||||
assert.deepStrictEqual(result, expected); | ||||||
}); | ||||||
|
||||||
test('when help arg with help value for lone long option is added, then add help text', () => { | ||||||
const args = ['--foo', 'bar']; | ||||||
const options = { foo: { type: 'string', help: 'help text' } }; | ||||||
const help = 'Description for some awesome stuff:'; | ||||||
const printUsage = help + '\n--foo <arg> help text'; | ||||||
const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; | ||||||
const result = parseArgs({ args, options, allowPositionals: true, help }); | ||||||
assert.deepStrictEqual(result, expected); | ||||||
}); | ||||||
|
||||||
test('when help arg with help value for lone long option and value is added, then add help text', () => { | ||||||
const args = ['--foo=bar']; | ||||||
const options = { foo: { type: 'string', help: 'help text' } }; | ||||||
const help = 'Description for some awesome stuff:'; | ||||||
const printUsage = help + '\n--foo <arg> help text'; | ||||||
const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; | ||||||
const result = parseArgs({ args, options, allowPositionals: true, help }); | ||||||
assert.deepStrictEqual(result, expected); | ||||||
}); | ||||||
|
||||||
test('when help arg with help values and without explicit help texts, then add help text', () => { | ||||||
const args = [ | ||||||
'-h', '-a', 'val1', | ||||||
]; | ||||||
const options = { | ||||||
help: { type: 'boolean', short: 'h', help: 'Prints command line options' }, | ||||||
alpha: { type: 'string', short: 'a', help: 'Alpha option help' }, | ||||||
beta: { type: 'boolean', short: 'b', help: 'Beta option help' }, | ||||||
charlie: { type: 'string', short: 'c' }, | ||||||
delta: { type: 'string', help: 'Delta option help' }, | ||||||
echo: { type: 'boolean', short: 'e', help: 'Echo option help' }, | ||||||
foxtrot: { type: 'string', help: 'Foxtrot option help' }, | ||||||
golf: { type: 'boolean', help: 'Golf option help' }, | ||||||
hotel: { type: 'string', help: 'Hotel option help' }, | ||||||
india: { type: 'string' }, | ||||||
juliet: { type: 'boolean', short: 'j', help: 'Juliet option help' }, | ||||||
looooooooooooooongHelpText: { | ||||||
type: 'string', | ||||||
short: 'L', | ||||||
help: 'Very long option help text for demonstration purposes' | ||||||
} | ||||||
}; | ||||||
const help = 'Description for some awesome stuff:'; | ||||||
|
||||||
const result = parseArgs({ args, options, help }); | ||||||
const printUsage = | ||||||
'Description for some awesome stuff:\n' + | ||||||
'-h, --help Prints command line options\n' + | ||||||
'-a, --alpha <arg> Alpha option help\n' + | ||||||
'-b, --beta Beta option help\n' + | ||||||
'-c, --charlie <arg>\n' + | ||||||
'--delta <arg> Delta option help\n' + | ||||||
'-e, --echo Echo option help\n' + | ||||||
'--foxtrot <arg> Foxtrot option help\n' + | ||||||
'--golf Golf option help\n' + | ||||||
'--hotel <arg> Hotel option help\n' + | ||||||
'--india <arg>\n' + | ||||||
'-j, --juliet Juliet option help\n' + | ||||||
'-L, --looooooooooooooongHelpText <arg>\n' + | ||||||
' Very long option help text for demonstration purposes'; | ||||||
|
||||||
assert.strictEqual(result.printUsage, printUsage); | ||||||
}); | ||||||
|
||||||
test('when help arg but no help text is available, then add help text', () => { | ||||||
const args = ['-a', 'val1', '--help']; | ||||||
const help = 'Description for some awesome stuff:'; | ||||||
const options = { alpha: { type: 'string', short: 'a' }, help: { type: 'boolean' } }; | ||||||
const printUsage = | ||||||
'Description for some awesome stuff:\n' + | ||||||
'-a, --alpha <arg>\n' + | ||||||
'--help'; | ||||||
|
||||||
const result = parseArgs({ args, options, help }); | ||||||
|
||||||
assert.strictEqual(result.printUsage, printUsage); | ||||||
}); |
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.
In some of the original discussion,
printUsage
was a function and hence the name. It was suggested that simpler to just return a string, which is what you have done here. So I think the name could be changed.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.
I personally think a function is better, because then it allows the implementation to defer formatting of the text if it wishes to.