Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
86083c3
lib: add help text support for options in parse_args
miguelmarcondesf Jun 28, 2025
1e2dda3
lib: improve readability for help text formatting method
miguelmarcondesf Jun 28, 2025
7178c4c
lib: add support for help text in parseArgs
miguelmarcondesf Jun 28, 2025
e5013a1
lib: add enableHelpPrinting option
miguelmarcondesf Jun 28, 2025
40f4e67
lib: enhance help printing logic to handle empty help text
miguelmarcondesf Jun 28, 2025
cee8d9d
lib: refactor help printing test to improve output handling and missi…
miguelmarcondesf Jun 28, 2025
07b8498
doc: add support for help text in options and enableHelpPrinting conf…
miguelmarcondesf Jun 28, 2025
13aed88
doc: update pull request URL
miguelmarcondesf Jun 28, 2025
707ebd0
lib: checks for help flag
miguelmarcondesf Jul 1, 2025
eadccbb
doc: update printUsage return type
miguelmarcondesf Jul 3, 2025
2be1787
lib: remove enableHelpPrinting option
miguelmarcondesf Jul 7, 2025
6c363f5
lib: refactor formatHelpTextForPrint and simplify printUsage logic
miguelmarcondesf Jul 12, 2025
213c061
test: improve tests description
miguelmarcondesf Jul 26, 2025
e6a219f
test: update return structure and improve tests descriptions
miguelmarcondesf Jul 26, 2025
c8923dd
test: fix lint
miguelmarcondesf Jul 26, 2025
40256fc
Update doc/api/util.md
miguelmarcondesf Aug 30, 2025
15b44d2
lib: improve formatting in formatHelpTextForPrint using StringPrototy…
miguelmarcondesf Aug 30, 2025
55b29d5
lib: add support for auto-injecting help option and return help text …
miguelmarcondesf Sep 7, 2025
f8d9140
test: enhance help option tests
miguelmarcondesf Sep 7, 2025
caf2840
doc: update util to enhance parseArgs help option functionality
miguelmarcondesf Sep 7, 2025
f6b6199
lib: removing returnHelpText and addHelpOption
miguelmarcondesf Nov 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1991,6 +1991,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
Expand Down Expand Up @@ -2031,6 +2035,7 @@ changes:
`true`, it must be an array. No default value is applied when the option
does appear in the arguments to be parsed, even if the provided value
is falsy.
* `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 @@ -2045,11 +2050,13 @@ 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.
* `helpText` {string | undefined} Formatted help text for all options provided.
* `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens)
section. Only returned if `config` includes `tokens: true`.

Expand Down Expand Up @@ -2097,6 +2104,85 @@ 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. When
general help text is provided, a help option is automatically added unless already present.

#### Simple usage

By default, providing general help text automatically adds a help option (`-h, --help`) and
returns formatted help text in `result.helpText`:

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

const options = {
foo: {
type: 'boolean',
short: 'f',
help: 'use the foo filter',
},
bar: {
type: 'string',
help: 'use the specified bar filter',
},
};

const result = parseArgs({
help: 'utility to control filters',
options,
});

if (result.values.help) {
console.log(result.helpText);
// Prints:
// utility to control filters
// -f, --foo use the foo filter
// --bar <arg> use the specified bar filter
// -h, --help Show help
}
```

#### Custom help option

You can override the auto-added help option by defining your own:

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

const options = {
foo: {
type: 'boolean',
short: 'f',
help: 'use the foo filter',
},
bar: {
type: 'string',
help: 'use the specified bar filter',
},
help: {
type: 'boolean',
Comment on lines +2164 to +2165
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the user wants their custom option to be called "ayuda", how would that disable the auto-added --help?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ljharb Good point, but in that case, everything we're using in relation to the help general and each help option would be impacted, considering some kind of translation.

If it's only related to injecting --help when the general help option isn't available, we can simply remove that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove what? I'm confused.

Personally, I think the help option should only ever be named "--help", and be a boolean, but others argued upthread that it should be customizable. However, now we don't have any way to know if the user is passing a "help" option or not, because it could be named anything.

That's why I suggested type: "help", which can only be a boolean, because then we only inject our own help option when no type help options are provided by the user.

short: '?',
help: 'display help',
},
};

const result = parseArgs({
help: 'utility to control filters',
options,
});

if (result.values.help) {
console.log(result.helpText);
// Prints:
// utility to control filters
// -f, --foo use the foo filter
// --bar <arg> use the specified bar filter
// -?, --help display help
}
```

### `parseArgs` `tokens`

Detailed parse information is available for adding custom behaviors by
Expand Down
77 changes: 75 additions & 2 deletions lib/internal/util/parse_args/parse_args.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
ObjectPrototypeHasOwnProperty: ObjectHasOwn,
StringPrototypeCharAt,
StringPrototypeIndexOf,
StringPrototypePadEnd,
StringPrototypeSlice,
StringPrototypeStartsWith,
} = primordials;
Expand Down Expand Up @@ -305,13 +306,63 @@ 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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>';
}

if (help) {
if (helpTextForPrint.length > layoutSpacing) {
helpTextForPrint += `\n${StringPrototypePadEnd('', layoutSpacing)}${help}`;
} else {
helpTextForPrint = `${StringPrototypePadEnd(helpTextForPrint, 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 };
let options = objectGetOwn(config, 'options') ?? { __proto__: null };
const help = objectGetOwn(config, 'help') ?? '';

const hasGenerateHelp = help.length > 0;
if (hasGenerateHelp && !ObjectHasOwn(options, 'help')) {
options = {
...options,
__proto__: null,
help: {
type: 'boolean',
short: 'h',
help: 'Show help',
},
};
}
// Bundle these up for passing to strict-mode checks.
const parseConfig = { args, strict, options, allowPositionals, allowNegative };

Expand All @@ -322,11 +373,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']);
Expand Down Expand Up @@ -362,6 +413,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 @@ -404,6 +460,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.helpText = printUsage;
}

return result;
};
Expand Down
153 changes: 153 additions & 0 deletions test/parallel/test-parse-args.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,3 +1062,156 @@ test('auto-detect --no-foo as negated when strict:false and allowNegative', () =
process.argv = holdArgv;
process.execArgv = holdExecArgv;
});

// Test help option
{
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 option has help text values but help arg value is not provided, then no help value appear', () => {
const args = ['-f', 'bar'];
const options = { foo: { type: 'string', short: 'f', help: 'help text' } };
const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [] };
const result = parseArgs({ args, options, allowPositionals: true });
assert.deepStrictEqual(result, expected);
});

test('when option has short and long flags, then both appear in usage with help option', () => {
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\n-h, --help Show help';
const expected = { helpText: printUsage, values: { __proto__: null, foo: 'bar' }, positionals: [] };
const result = parseArgs({ args, options, allowPositionals: true, help });
assert.deepStrictEqual(result, expected);
});

test('when options has short group flags, then both appear in usage with help option', () => {
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\n' +
'-h, --help Show help';
const expected = { helpText: printUsage, values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [] };
const result = parseArgs({ args, options, allowPositionals: true, help });
assert.deepStrictEqual(result, expected);
});

test('when options has short flag with value, then both appear in usage with help option', () => {
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\n-h, --help Show help';
const expected = { helpText: printUsage, values: { __proto__: null, foo: 'FILE' }, positionals: [] };
const result = parseArgs({ args, options, allowPositionals: true, help });
assert.deepStrictEqual(result, expected);
});

test('when options has long flag, then it appear in usage with help option', () => {
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\n-h, --help Show help';
const expected = { helpText: printUsage, values: { __proto__: null, foo: 'bar' }, positionals: [] };
const result = parseArgs({ args, options, allowPositionals: true, help });
assert.deepStrictEqual(result, expected);
});

test('when options has long flag with value, then both appear in usage with help option', () => {
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\n-h, --help Show help';
const expected = { helpText: printUsage, values: { __proto__: null, foo: 'bar' }, positionals: [] };
const result = parseArgs({ args, options, allowPositionals: true, help });
assert.deepStrictEqual(result, expected);
});

test('when options has help values with and without explicit texts, then all appear in usage', () => {
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.helpText, printUsage);
});

test('when general help text and options with no help values, then all appear in usage', () => {
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.helpText, printUsage);
});

test('auto-injects help option when no existing help option', () => {
const args = ['--foo', 'bar'];
const options = { foo: { type: 'string', help: 'use the foo filter' } };
const help = 'utility to control filters';

const result = parseArgs({ args, options, help });

assert.ok(result.helpText.includes('-h, --help Show help'));
const resultWithHelp = parseArgs({ args: ['--help'], options, help });
assert.strictEqual(resultWithHelp.values.help, true);
});
}
Loading