Skip to content
109 changes: 109 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1921,6 +1921,10 @@
- v18.3.0
- v16.17.0
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/58875

Check warning on line 1926 in doc/api/util.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: Add support for help text in options and enableHelpPrinting config.
- version:
- v22.4.0
- v20.16.0
Expand Down Expand Up @@ -1959,6 +1963,7 @@
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`.
Expand All @@ -1973,13 +1978,19 @@
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.
* `enableHelpPrinting` {boolean} When `true`, if any options have help text
configured, the help will be printed to stdout and the process will exit
with code 0. **Default:** `false`.

* 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 options that have
help text configured. Only included if help text is available and `enableHelpPrinting` is `false`.

Provides a higher level API for command-line argument parsing than interacting
with `process.argv` directly. Takes a specification for the expected arguments
Expand Down Expand Up @@ -2025,6 +2036,104 @@
// Prints: [Object: null prototype] { foo: true, bar: 'b' } []
```

### `parseArgs` help text

`parseArgs` can generate and display help text for command-line options. To enable
this functionality, add `help` text to individual options and optionally provide
general help text via the `help` config property.

```mjs
import { parseArgs } from 'node:util';

const options = {
verbose: {
type: 'boolean',
short: 'v',
help: 'Enable verbose output',
},
file: {
type: 'string',
short: 'f',
help: 'Input file path',
},
output: {
type: 'string',
help: 'Output directory',
},
};

// Get help text in result
const result = parseArgs({
options,
help: 'My CLI Tool v1.0\n\nProcess files with various options.',
});

if (result.printUsage) {
console.log(result.printUsage.join('\n'));
// Prints:
// My CLI Tool v1.0
//
// Process files with various options.
// -v, --verbose Enable verbose output
// -f, --file <arg> Input file path
// --output <arg> Output directory
}

// Or automatically print help and exit
parseArgs({
options,
help: 'My CLI Tool v1.0\n\nProcess files with various options.',
enableHelpPrinting: true,
});
// Prints help and exits with code 0
```

```cjs
const { parseArgs } = require('node:util');

const options = {
verbose: {
type: 'boolean',
short: 'v',
help: 'Enable verbose output',
},
file: {
type: 'string',
short: 'f',
help: 'Input file path',
},
output: {
type: 'string',
help: 'Output directory',
},
};

// Get help text in result
const result = parseArgs({
options,
help: 'My CLI Tool v1.0\n\nProcess files with various options.',
});

if (result.printUsage) {
console.log(result.printUsage.join('\n'));
// Prints:
// My CLI Tool v1.0
//
// Process files with various options.
// -v, --verbose Enable verbose output
// -f, --file <arg> Input file path
// --output <arg> Output directory
}

// Or automatically print help and exit
parseArgs({
options,
help: 'My CLI Tool v1.0\n\nProcess files with various options.',
enableHelpPrinting: true,
});
// Prints help and exits with code 0
```

### `parseArgs` `tokens`

Detailed parse information is available for adding custom behaviors by
Expand Down
87 changes: 82 additions & 5 deletions lib/internal/util/parse_args/parse_args.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const {
} = require('internal/validators');

const {
findHelpValueForOption,
findLongOptionForShort,
isLoneLongOption,
isLoneShortOption,
Expand Down Expand Up @@ -218,6 +219,7 @@ function argsToTokens(args, options) {
// e.g. '-f'
const shortOption = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(shortOption, options);
const helpValue = findHelpValueForOption(longOption, options);
let value;
let inlineValue;
if (optionsGetOwn(options, longOption, 'type') === 'string' &&
Expand All @@ -229,7 +231,7 @@ function argsToTokens(args, options) {
ArrayPrototypePush(
tokens,
{ kind: 'option', name: longOption, rawName: arg,
index, value, inlineValue });
index, value, inlineValue, ...(helpValue !== undefined && { help: helpValue }) });
if (value != null) ++index;
continue;
}
Expand Down Expand Up @@ -261,16 +263,19 @@ function argsToTokens(args, options) {
const shortOption = StringPrototypeCharAt(arg, 1);
const longOption = findLongOptionForShort(shortOption, options);
const value = StringPrototypeSlice(arg, 2);
const helpValue = findHelpValueForOption(longOption, options);
ArrayPrototypePush(
tokens,
{ kind: 'option', name: longOption, rawName: `-${shortOption}`,
index, value, inlineValue: true });
index, value, inlineValue: true,
...(helpValue !== undefined && { help: helpValue }) });
continue;
}

if (isLoneLongOption(arg)) {
// e.g. '--foo'
const longOption = StringPrototypeSlice(arg, 2);
const helpValue = findHelpValueForOption(longOption, options);
let value;
let inlineValue;
if (optionsGetOwn(options, longOption, 'type') === 'string' &&
Expand All @@ -282,7 +287,7 @@ function argsToTokens(args, options) {
ArrayPrototypePush(
tokens,
{ kind: 'option', name: longOption, rawName: arg,
index, value, inlineValue });
index, value, inlineValue, ...(helpValue !== undefined && { help: helpValue }) });
if (value != null) ++index;
continue;
}
Expand All @@ -292,10 +297,11 @@ function argsToTokens(args, options) {
const equalIndex = StringPrototypeIndexOf(arg, '=');
const longOption = StringPrototypeSlice(arg, 2, equalIndex);
const value = StringPrototypeSlice(arg, equalIndex + 1);
const helpValue = findHelpValueForOption(longOption, options);
ArrayPrototypePush(
tokens,
{ kind: 'option', name: longOption, rawName: `--${longOption}`,
index, value, inlineValue: true });
index, value, inlineValue: true, ...(helpValue !== undefined && { help: helpValue }) });
continue;
}

Expand All @@ -304,13 +310,52 @@ 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;

const shortOption = objectGetOwn(optionConfig, 'short');
const type = objectGetOwn(optionConfig, 'type');
const help = objectGetOwn(optionConfig, 'help');

let helpTextForPrint = '';
if (help) {
if (shortOption) {
helpTextForPrint += `-${shortOption}, `;
}
helpTextForPrint += `--${longOption}`;
if (type === 'string') {
helpTextForPrint += ' <arg>';
} else if (type === 'boolean') {
helpTextForPrint += '';
}
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') ?? '';
const enableHelpPrinting = objectGetOwn(config, 'enableHelpPrinting') ?? false;
// Bundle these up for passing to strict-mode checks.
const parseConfig = { args, strict, options, allowPositionals, allowNegative };

Expand All @@ -321,11 +366,12 @@ const parseArgs = (config = kEmptyObject) => {
validateBoolean(returnTokens, 'tokens');
validateBoolean(allowNegative, 'allowNegative');
validateObject(options, 'options');
validateString(help, 'help');
validateBoolean(enableHelpPrinting, 'enableHelpPrinting');
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']);
Expand Down Expand Up @@ -361,6 +407,11 @@ const parseArgs = (config = kEmptyObject) => {
}
validator(defaultValue, `options.${longOption}.default`);
}

const helpOption = objectGetOwn(optionConfig, 'help');
if (ObjectHasOwn(optionConfig, 'help')) {
validateString(helpOption, `options.${longOption}.help`);
}
},
);

Expand Down Expand Up @@ -403,6 +454,32 @@ const parseArgs = (config = kEmptyObject) => {
}
});

// Phase 4: generate print usage for each option
const printUsage = [];
if (help) {
ArrayPrototypePush(printUsage, help);
}
ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => {
const helpTextForPrint = formatHelpTextForPrint(longOption, optionConfig);

if (helpTextForPrint) {
ArrayPrototypePush(printUsage, helpTextForPrint);
}
});

if (enableHelpPrinting) {
const console = require('internal/console/global');
if (printUsage.length > 0 || help) {
ArrayPrototypeForEach(printUsage, (line) => {
console.log(line);
});
} else {
console.log('No help text available.');
}
process.exit(0);
} else if (printUsage.length > 0) {
result.printUsage = printUsage;
}

return result;
};
Expand Down
16 changes: 16 additions & 0 deletions lib/internal/util/parse_args/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ function findLongOptionForShort(shortOption, options) {
return longOptionEntry?.[0] ?? shortOption;
}

/**
* Find the help value associated with a long option.
* @param {string} longOption
* @param {object} options
* @returns {string|undefined} - the help value or undefined if not found
*/
function findHelpValueForOption(longOption, options) {
validateObject(options, 'options');
if (ObjectHasOwn(options, longOption)) {
return objectGetOwn(options[longOption], 'help');
}

return undefined;
}

/**
* Check if the given option includes a default value
* and that option has not been set by the input args.
Expand All @@ -183,6 +198,7 @@ function useDefaultValueOption(longOption, optionConfig, values) {
}

module.exports = {
findHelpValueForOption,
findLongOptionForShort,
isLoneLongOption,
isLoneShortOption,
Expand Down
Loading
Loading