From 86083c330980b1b10c7d2c05d6245b2249382b9e Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 27 Jun 2025 21:40:20 -0300 Subject: [PATCH 01/21] lib: add help text support for options in parse_args --- lib/internal/util/parse_args/parse_args.js | 48 +++++++++++++++++-- lib/internal/util/parse_args/utils.js | 16 +++++++ test/parallel/test-parse-args.mjs | 56 ++++++++++++++++++++++ 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 95420ac830d9a3..468acc93213f69 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -28,6 +28,7 @@ const { } = require('internal/validators'); const { + findHelpValueForOption, findLongOptionForShort, isLoneLongOption, isLoneShortOption, @@ -219,6 +220,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' && @@ -230,7 +232,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; } @@ -262,16 +264,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' && @@ -283,7 +288,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; } @@ -293,10 +298,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; } @@ -326,7 +332,6 @@ const parseArgs = (config = kEmptyObject) => { 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']); @@ -362,6 +367,11 @@ const parseArgs = (config = kEmptyObject) => { } validator(defaultValue, `options.${longOption}.default`); } + + const helpOption = objectGetOwn(optionConfig, 'help'); + if (ObjectHasOwn(optionConfig, 'help')) { + validateString(helpOption, `options.${longOption}.help`); + } }, ); @@ -404,6 +414,34 @@ const parseArgs = (config = kEmptyObject) => { } }); + const usage = []; + ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { + const shortOption = objectGetOwn(optionConfig, 'short'); + const type = objectGetOwn(optionConfig, 'type'); + const help = objectGetOwn(optionConfig, 'help'); + + let usageLine = ''; + if (help) { + if (shortOption) { + usageLine += `-${shortOption}, `; + } + usageLine += `--${longOption}`; + if (type === 'string') { + usageLine += ' '; + } else if (type === 'boolean') { + usageLine += ''; + } + usageLine = usageLine.padEnd(30) + help; + } + + if (usageLine) { + ArrayPrototypePush(usage, usageLine); + } + }); + + if (usage.length > 0) { + result.usage = usage; + } return result; }; diff --git a/lib/internal/util/parse_args/utils.js b/lib/internal/util/parse_args/utils.js index 8b7c1cc20a517d..f47967ecb4ffef 100644 --- a/lib/internal/util/parse_args/utils.js +++ b/lib/internal/util/parse_args/utils.js @@ -192,6 +192,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. @@ -206,6 +221,7 @@ function useDefaultValueOption(longOption, optionConfig, values) { } module.exports = { + findHelpValueForOption, findLongOptionForShort, isLoneLongOption, isLoneShortOption, diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index e79434bdc6bbbf..2553ade1258fa9 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1062,3 +1062,59 @@ test('auto-detect --no-foo as negated when strict:false and allowNegative', () = process.argv = holdArgv; process.execArgv = holdExecArgv; }); + +test('help value 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 value for lone short option is added, then add help text', () => { + const args = ['-f', 'bar']; + const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; + const usage = ['-f, --foo help text']; + const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], usage }; + const result = parseArgs({ args, options, allowPositionals: true }); + assert.deepStrictEqual(result, expected); +}); + +test('when 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 usage = ['-f, --foo help text', + '-m, --moo help text']; + const expected = { values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [], usage }; + const result = parseArgs({ args, options, allowPositionals: true }); + assert.deepStrictEqual(result, expected); +}); + +test('when 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 usage = ['-f, --foo help text']; + const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [], usage }; + const result = parseArgs({ args, options, allowPositionals: true }); + assert.deepStrictEqual(result, expected); +}); + +test('when 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 usage = ['--foo help text']; + const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], usage }; + const result = parseArgs({ args, options, allowPositionals: true }); + assert.deepStrictEqual(result, expected); +}); + +test('when 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 usage = ['--foo help text']; + const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], usage }; + const result = parseArgs({ args, options, allowPositionals: true }); + assert.deepStrictEqual(result, expected); +}); From 1e2dda32aed8d148baf9c431e62b31f6bce4cb7e Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 12:59:29 -0300 Subject: [PATCH 02/21] lib: improve readability for help text formatting method --- lib/internal/util/parse_args/parse_args.js | 60 ++++++++++++++-------- test/parallel/test-parse-args.mjs | 20 ++++---- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 468acc93213f69..df9de8a3bd9bc6 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -311,6 +311,37 @@ 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 help text' + */ +function formatHelpTextForPrint(longOption, optionConfig) { + 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 += ' '; + } else if (type === 'boolean') { + helpTextForPrint += ''; + } + helpTextForPrint = helpTextForPrint.padEnd(30) + help; + } + + return helpTextForPrint; +} + const parseArgs = (config = kEmptyObject) => { const args = objectGetOwn(config, 'args') ?? getMainArgs(); const strict = objectGetOwn(config, 'strict') ?? true; @@ -414,33 +445,18 @@ const parseArgs = (config = kEmptyObject) => { } }); - const usage = []; + // Phase 4: generate print usage for each option + const printUsage = []; ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { - const shortOption = objectGetOwn(optionConfig, 'short'); - const type = objectGetOwn(optionConfig, 'type'); - const help = objectGetOwn(optionConfig, 'help'); - - let usageLine = ''; - if (help) { - if (shortOption) { - usageLine += `-${shortOption}, `; - } - usageLine += `--${longOption}`; - if (type === 'string') { - usageLine += ' '; - } else if (type === 'boolean') { - usageLine += ''; - } - usageLine = usageLine.padEnd(30) + help; - } + const helpTextForPrint = formatHelpTextForPrint(longOption, optionConfig); - if (usageLine) { - ArrayPrototypePush(usage, usageLine); + if (helpTextForPrint) { + ArrayPrototypePush(printUsage, helpTextForPrint); } }); - if (usage.length > 0) { - result.usage = usage; + if (printUsage.length > 0) { + result.printUsage = printUsage; } return result; diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 2553ade1258fa9..418958757dffdb 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1075,8 +1075,8 @@ test('help value must be a string', () => { test('when help value for lone short option is added, then add help text', () => { const args = ['-f', 'bar']; const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; - const usage = ['-f, --foo help text']; - const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], usage }; + const printUsage = ['-f, --foo help text']; + const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); }); @@ -1085,9 +1085,9 @@ test('when 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 usage = ['-f, --foo help text', + const printUsage = ['-f, --foo help text', '-m, --moo help text']; - const expected = { values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [], usage }; + const expected = { values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); }); @@ -1095,8 +1095,8 @@ test('when help value for short group option is added, then add help text', () = test('when 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 usage = ['-f, --foo help text']; - const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [], usage }; + const printUsage = ['-f, --foo help text']; + const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); }); @@ -1104,8 +1104,8 @@ test('when help value for short option and value is added, then add help text', test('when 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 usage = ['--foo help text']; - const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], usage }; + const printUsage = ['--foo help text']; + const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); }); @@ -1113,8 +1113,8 @@ test('when help value for lone long option is added, then add help text', () => test('when 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 usage = ['--foo help text']; - const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], usage }; + const printUsage = ['--foo help text']; + const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); }); From 7178c4c458e7907445331ba2b3d4296501b940bc Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 13:32:09 -0300 Subject: [PATCH 03/21] lib: add support for help text in parseArgs --- lib/internal/util/parse_args/parse_args.js | 7 ++++++- test/parallel/test-parse-args.mjs | 24 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index df9de8a3bd9bc6..8725adf2556fce 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -314,7 +314,7 @@ function argsToTokens(args, options) { /** * Format help text for printing. * @param {string} longOption - long option name e.g. 'foo' - * @param {Object} optionConfig - option config from parseArgs({ options }) + * @param {object} optionConfig - option config from parseArgs({ options }) * @returns {string} formatted help text for printing * @example * formatHelpTextForPrint('foo', { type: 'string', help: 'help text' }) @@ -349,6 +349,7 @@ const parseArgs = (config = kEmptyObject) => { 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 }; @@ -359,6 +360,7 @@ const parseArgs = (config = kEmptyObject) => { validateBoolean(returnTokens, 'tokens'); validateBoolean(allowNegative, 'allowNegative'); validateObject(options, 'options'); + validateString(help, 'help'); ArrayPrototypeForEach( ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { @@ -447,6 +449,9 @@ 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); diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 418958757dffdb..886b403e46a320 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1063,7 +1063,7 @@ test('auto-detect --no-foo as negated when strict:false and allowNegative', () = process.execArgv = holdExecArgv; }); -test('help value must be a string', () => { +test('help value for option must be a string', () => { const args = []; const options = { alpha: { type: 'string', help: true } }; assert.throws(() => { @@ -1086,7 +1086,7 @@ test('when help value for short group option is added, then add help text', () = const options = { foo: { type: 'boolean', short: 'f', help: 'help text' }, moo: { type: 'string', short: 'm', help: 'help text' } }; const printUsage = ['-f, --foo help text', - '-m, --moo help text']; + '-m, --moo help text']; const expected = { values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); @@ -1118,3 +1118,23 @@ test('when help value for lone long option and value is added, then add help tex const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); }); + +test('help value 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('when help value is added, then add initial help text', () => { + const args = ['-f', 'bar']; + const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; + const help = 'Description for some awesome stuff:'; + const printUsage = [help, '-f, --foo help text']; + const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; + const result = parseArgs({ args, options, help }); + assert.deepStrictEqual(result, expected); +}); From e5013a170d7a869442eb1c6853713e893417e3ae Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:15:33 -0300 Subject: [PATCH 04/21] lib: add enableHelpPrinting option --- lib/internal/util/parse_args/parse_args.js | 19 +++++- test/parallel/test-parse-args.mjs | 77 +++++++++++++++++++++- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 8725adf2556fce..75208673250d14 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -321,6 +321,8 @@ function argsToTokens(args, options) { * // returns '--foo help text' */ function formatHelpTextForPrint(longOption, optionConfig) { + const layoutSpacing = 30; + const shortOption = objectGetOwn(optionConfig, 'short'); const type = objectGetOwn(optionConfig, 'type'); const help = objectGetOwn(optionConfig, 'help'); @@ -336,7 +338,11 @@ function formatHelpTextForPrint(longOption, optionConfig) { } else if (type === 'boolean') { helpTextForPrint += ''; } - helpTextForPrint = helpTextForPrint.padEnd(30) + help; + if (helpTextForPrint.length > layoutSpacing) { + helpTextForPrint += '\n' + ''.padEnd(layoutSpacing) + help; + } else { + helpTextForPrint = helpTextForPrint.padEnd(layoutSpacing) + help; + } } return helpTextForPrint; @@ -350,6 +356,7 @@ const parseArgs = (config = kEmptyObject) => { 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 }; @@ -361,6 +368,7 @@ const parseArgs = (config = kEmptyObject) => { validateBoolean(allowNegative, 'allowNegative'); validateObject(options, 'options'); validateString(help, 'help'); + validateBoolean(enableHelpPrinting, 'enableHelpPrinting'); ArrayPrototypeForEach( ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { @@ -460,7 +468,14 @@ const parseArgs = (config = kEmptyObject) => { } }); - if (printUsage.length > 0) { + if (enableHelpPrinting && printUsage.length > 0) { + const console = require('internal/console/global'); + ArrayPrototypeForEach(printUsage, (line) => { + console.log(line); + }); + + process.exit(0); + } else if (printUsage.length > 0) { result.printUsage = printUsage; } diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 886b403e46a320..836eb4f540c8cd 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1119,7 +1119,7 @@ test('when help value for lone long option and value is added, then add help tex assert.deepStrictEqual(result, expected); }); -test('help value must be a string', () => { +test('help value config must be a string', () => { const args = ['-f', 'bar']; const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; const help = true; @@ -1138,3 +1138,78 @@ test('when help value is added, then add initial help text', () => { const result = parseArgs({ args, options, help }); assert.deepStrictEqual(result, expected); }); + +test('enableHelpPrinting config must be a boolean', () => { + const args = ['-f', 'bar']; + const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; + const help = 'Description for some awesome stuff:'; + const enableHelpPrinting = 'not a boolean'; + assert.throws(() => { + parseArgs({ args, options, help, enableHelpPrinting }); + }, /The "enableHelpPrinting" argument must be of type boolean/ + ); +}); + +test('when enableHelpPrinting config is true, print all help text and exit', () => { + const originalLog = console.log; + const originalExit = process.exit; + + let output = ''; + let exitCode = null; + + console.log = (message) => { + output += message + '\n'; + }; + + process.exit = (code) => { + exitCode = code; + }; + + try { + const args = [ + '-a', 'val1', '--beta', '-c', 'val3', '--delta', 'val4', '-e', + '--foxtrot', 'val6', '--golf', '-h', 'val8', '--india', 'val9', '-j', + ]; + const options = { + alpha: { type: 'string', short: 'a', help: 'Alpha option help' }, + beta: { type: 'boolean', short: 'b', help: 'Beta option help' }, + charlie: { type: 'string', short: 'c', help: 'Charlie option help' }, + 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', short: 'h', help: 'Hotel option help' }, + india: { type: 'string', help: 'India option help' }, + 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:'; + + parseArgs({ args, options, help, enableHelpPrinting: true }); + } finally { + console.log = originalLog; + process.exit = originalExit; + } + + const expectedOutput = + 'Description for some awesome stuff:\n' + + '-a, --alpha Alpha option help\n' + + '-b, --beta Beta option help\n' + + '-c, --charlie Charlie option help\n' + + '--delta Delta option help\n' + + '-e, --echo Echo option help\n' + + '--foxtrot Foxtrot option help\n' + + '--golf Golf option help\n' + + '-h, --hotel Hotel option help\n' + + '--india India option help\n' + + '-j, --juliet Juliet option help\n' + + '-L, --looooooooooooooongHelpText \n' + + ' Very long option help text for demonstration purposes\n'; + + assert.strictEqual(exitCode, 0); + assert.strictEqual(output, expectedOutput); +}); From 40f4e672d8ddb7e3a2e435f59766f09220530fe6 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:36:16 -0300 Subject: [PATCH 05/21] lib: enhance help printing logic to handle empty help text --- lib/internal/util/parse_args/parse_args.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 75208673250d14..210c9a3448d3f0 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -468,12 +468,15 @@ const parseArgs = (config = kEmptyObject) => { } }); - if (enableHelpPrinting && printUsage.length > 0) { + if (enableHelpPrinting) { const console = require('internal/console/global'); - ArrayPrototypeForEach(printUsage, (line) => { - console.log(line); - }); - + 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; From cee8d9d75b150049b7f44fc859030a66564dc5db Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:36:31 -0300 Subject: [PATCH 06/21] lib: refactor help printing test to improve output handling and missing help text --- test/parallel/test-parse-args.mjs | 36 ++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 836eb4f540c8cd..3dbfbdf6d9932b 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1150,7 +1150,7 @@ test('enableHelpPrinting config must be a boolean', () => { ); }); -test('when enableHelpPrinting config is true, print all help text and exit', () => { +function setupConsoleAndExit() { const originalLog = console.log; const originalExit = process.exit; @@ -1165,6 +1165,17 @@ test('when enableHelpPrinting config is true, print all help text and exit', () exitCode = code; }; + function restore() { + console.log = originalLog; + process.exit = originalExit; + } + + return { getOutput: () => output, getExitCode: () => exitCode, restore }; +} + +test('when enableHelpPrinting config is true, print all help text and exit', () => { + const { getOutput, getExitCode, restore } = setupConsoleAndExit(); + try { const args = [ '-a', 'val1', '--beta', '-c', 'val3', '--delta', 'val4', '-e', @@ -1191,8 +1202,7 @@ test('when enableHelpPrinting config is true, print all help text and exit', () parseArgs({ args, options, help, enableHelpPrinting: true }); } finally { - console.log = originalLog; - process.exit = originalExit; + restore(); } const expectedOutput = @@ -1210,6 +1220,22 @@ test('when enableHelpPrinting config is true, print all help text and exit', () '-L, --looooooooooooooongHelpText \n' + ' Very long option help text for demonstration purposes\n'; - assert.strictEqual(exitCode, 0); - assert.strictEqual(output, expectedOutput); + assert.strictEqual(getExitCode(), 0); + assert.strictEqual(getOutput(), expectedOutput); +}); + +test('when enableHelpPrinting config is true, but no help text is available', () => { + const { getOutput, getExitCode, restore } = setupConsoleAndExit(); + + try { + const args = ['-a', 'val1']; + const options = { alpha: { type: 'string', short: 'a' } }; + + parseArgs({ args, options, enableHelpPrinting: true }); + } finally { + restore(); + } + + assert.strictEqual(getExitCode(), 0); + assert.strictEqual(getOutput(), 'No help text available.\n'); }); From 07b8498f576eeb02932638de491254442e0ee8bb Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:40:23 -0300 Subject: [PATCH 07/21] doc: add support for help text in options and enableHelpPrinting configuration --- doc/api/util.md | 109 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/doc/api/util.md b/doc/api/util.md index 9cae613bc9faa2..eb4e5eebb7c1e4 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1991,6 +1991,10 @@ added: - v18.3.0 - v16.17.0 changes: + - version: + - REPLACEME + pr-url: https://github.com/nodejs/node/pull/000000 + description: Add support for help text in options and enableHelpPrinting config. - version: - v22.4.0 - v20.16.0 @@ -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`. @@ -2045,6 +2050,10 @@ 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. + * `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} @@ -2052,6 +2061,8 @@ changes: * `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 @@ -2097,6 +2108,104 @@ console.log(values, positionals); // 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 Input file path + // --output 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 Input file path + // --output 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 From 13aed88a3a5cf1950379e0beadba954d649f238d Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:51:38 -0300 Subject: [PATCH 08/21] doc: update pull request URL --- doc/api/util.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index eb4e5eebb7c1e4..ecf645347c8fd7 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1993,7 +1993,7 @@ added: changes: - version: - REPLACEME - pr-url: https://github.com/nodejs/node/pull/000000 + pr-url: https://github.com/nodejs/node/pull/58875 description: Add support for help text in options and enableHelpPrinting config. - version: - v22.4.0 @@ -2147,8 +2147,8 @@ if (result.printUsage) { // // Process files with various options. // -v, --verbose Enable verbose output - // -f, --file Input file path - // --output Output directory + // -f, --file Input file path + // --output Output directory } // Or automatically print help and exit @@ -2193,8 +2193,8 @@ if (result.printUsage) { // // Process files with various options. // -v, --verbose Enable verbose output - // -f, --file Input file path - // --output Output directory + // -f, --file Input file path + // --output Output directory } // Or automatically print help and exit From 707ebd0a1a50702c76f67c3811b333e92060f4fa Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Tue, 1 Jul 2025 19:58:38 -0300 Subject: [PATCH 09/21] lib: checks for help flag --- doc/api/util.md | 28 +++++++++--------- lib/internal/util/parse_args/parse_args.js | 30 +++++++++----------- lib/internal/util/parse_args/utils.js | 16 ----------- test/parallel/test-parse-args.mjs | 33 +++++++++++----------- 4 files changed, 43 insertions(+), 64 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index ecf645347c8fd7..0ed00560e91dec 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -2052,7 +2052,7 @@ changes: **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 + configured and a `--help` entry was provided, the help will be printed to stdout and the process will exit with code 0. **Default:** `false`. * Returns: {Object} The parsed command line arguments: @@ -2123,10 +2123,10 @@ const options = { short: 'v', help: 'Enable verbose output', }, - file: { - type: 'string', - short: 'f', - help: 'Input file path', + help: { + type: 'boolean', + short: 'h', + help: 'Prints command line options', }, output: { type: 'string', @@ -2134,20 +2134,20 @@ const options = { }, }; -// Get help text in result +// 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) { - console.log(result.printUsage.join('\n')); + console.log(result.printUsage); // Prints: // My CLI Tool v1.0 // // Process files with various options. // -v, --verbose Enable verbose output - // -f, --file Input file path + // -h, --help. Prints command line options // --output Output directory } @@ -2169,10 +2169,10 @@ const options = { short: 'v', help: 'Enable verbose output', }, - file: { - type: 'string', - short: 'f', - help: 'Input file path', + help: { + type: 'boolean', + short: 'h', + help: 'Prints command line options', }, output: { type: 'string', @@ -2187,13 +2187,13 @@ const result = parseArgs({ }); if (result.printUsage) { - console.log(result.printUsage.join('\n')); + console.log(result.printUsage); // Prints: // My CLI Tool v1.0 // // Process files with various options. // -v, --verbose Enable verbose output - // -f, --file Input file path + // -h, --help. Prints command line options // --output Output directory } diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 210c9a3448d3f0..c451e37b7b005d 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -28,7 +28,6 @@ const { } = require('internal/validators'); const { - findHelpValueForOption, findLongOptionForShort, isLoneLongOption, isLoneShortOption, @@ -220,7 +219,6 @@ 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' && @@ -232,7 +230,7 @@ function argsToTokens(args, options) { ArrayPrototypePush( tokens, { kind: 'option', name: longOption, rawName: arg, - index, value, inlineValue, ...(helpValue !== undefined && { help: helpValue }) }); + index, value, inlineValue }); if (value != null) ++index; continue; } @@ -264,19 +262,16 @@ 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, - ...(helpValue !== undefined && { help: helpValue }) }); + index, value, inlineValue: true }); 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' && @@ -288,7 +283,7 @@ function argsToTokens(args, options) { ArrayPrototypePush( tokens, { kind: 'option', name: longOption, rawName: arg, - index, value, inlineValue, ...(helpValue !== undefined && { help: helpValue }) }); + index, value, inlineValue }); if (value != null) ++index; continue; } @@ -298,11 +293,10 @@ 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, ...(helpValue !== undefined && { help: helpValue }) }); + index, value, inlineValue: true }); continue; } @@ -456,24 +450,26 @@ const parseArgs = (config = kEmptyObject) => { }); // Phase 4: generate print usage for each option - const printUsage = []; + let printUsage = ''; if (help) { - ArrayPrototypePush(printUsage, help); + printUsage += help; } ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { const helpTextForPrint = formatHelpTextForPrint(longOption, optionConfig); if (helpTextForPrint) { - ArrayPrototypePush(printUsage, helpTextForPrint); + if (printUsage.length > 0) { + printUsage += '\n'; + } + printUsage += helpTextForPrint; } }); - if (enableHelpPrinting) { + const helpRequested = result.values.help; + if (enableHelpPrinting && helpRequested) { const console = require('internal/console/global'); if (printUsage.length > 0 || help) { - ArrayPrototypeForEach(printUsage, (line) => { - console.log(line); - }); + console.log(printUsage); } else { console.log('No help text available.'); } diff --git a/lib/internal/util/parse_args/utils.js b/lib/internal/util/parse_args/utils.js index f47967ecb4ffef..8b7c1cc20a517d 100644 --- a/lib/internal/util/parse_args/utils.js +++ b/lib/internal/util/parse_args/utils.js @@ -192,21 +192,6 @@ 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. @@ -221,7 +206,6 @@ function useDefaultValueOption(longOption, optionConfig, values) { } module.exports = { - findHelpValueForOption, findLongOptionForShort, isLoneLongOption, isLoneShortOption, diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 3dbfbdf6d9932b..a7ce95c307a7a4 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1075,7 +1075,7 @@ test('help value for option must be a string', () => { test('when help value for lone short option is added, then add help text', () => { const args = ['-f', 'bar']; const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; - const printUsage = ['-f, --foo help text']; + const printUsage = '-f, --foo help text'; const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); @@ -1085,8 +1085,7 @@ test('when 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 printUsage = ['-f, --foo help text', - '-m, --moo help text']; + const printUsage = '-f, --foo help text\n-m, --moo help text'; const expected = { values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); @@ -1095,7 +1094,7 @@ test('when help value for short group option is added, then add help text', () = test('when 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 printUsage = ['-f, --foo help text']; + const printUsage = '-f, --foo help text'; const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); @@ -1104,7 +1103,7 @@ test('when help value for short option and value is added, then add help text', test('when 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 printUsage = ['--foo help text']; + const printUsage = '--foo help text'; const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); @@ -1113,7 +1112,7 @@ test('when help value for lone long option is added, then add help text', () => test('when 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 printUsage = ['--foo help text']; + const printUsage = '--foo help text'; const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, allowPositionals: true }); assert.deepStrictEqual(result, expected); @@ -1133,7 +1132,7 @@ test('when help value is added, then add initial help text', () => { const args = ['-f', 'bar']; const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; const help = 'Description for some awesome stuff:'; - const printUsage = [help, '-f, --foo help text']; + const printUsage = help + '\n-f, --foo help text'; const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; const result = parseArgs({ args, options, help }); assert.deepStrictEqual(result, expected); @@ -1178,19 +1177,20 @@ test('when enableHelpPrinting config is true, print all help text and exit', () try { const args = [ - '-a', 'val1', '--beta', '-c', 'val3', '--delta', 'val4', '-e', - '--foxtrot', 'val6', '--golf', '-h', 'val8', '--india', 'val9', '-j', + '-h', '-a', 'val1', '--beta', '-c', 'val3', '--delta', 'val4', '-e', + '--foxtrot', 'val6', '--golf', '--hotel', 'val8', '--india', 'val9', '-j', ]; 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', help: 'Charlie 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', short: 'h', help: 'Hotel option help' }, - india: { type: 'string', help: 'India option help' }, + hotel: { type: 'string', help: 'Hotel option help' }, + india: { type: 'string' }, juliet: { type: 'boolean', short: 'j', help: 'Juliet option help' }, looooooooooooooongHelpText: { type: 'string', @@ -1207,15 +1207,14 @@ test('when enableHelpPrinting config is true, print all help text and exit', () const expectedOutput = 'Description for some awesome stuff:\n' + + '-h, --help Prints command line options\n' + '-a, --alpha Alpha option help\n' + '-b, --beta Beta option help\n' + - '-c, --charlie Charlie option help\n' + '--delta Delta option help\n' + '-e, --echo Echo option help\n' + '--foxtrot Foxtrot option help\n' + '--golf Golf option help\n' + - '-h, --hotel Hotel option help\n' + - '--india India option help\n' + + '--hotel Hotel option help\n' + '-j, --juliet Juliet option help\n' + '-L, --looooooooooooooongHelpText \n' + ' Very long option help text for demonstration purposes\n'; @@ -1228,8 +1227,8 @@ test('when enableHelpPrinting config is true, but no help text is available', () const { getOutput, getExitCode, restore } = setupConsoleAndExit(); try { - const args = ['-a', 'val1']; - const options = { alpha: { type: 'string', short: 'a' } }; + const args = ['-a', 'val1', '--help']; + const options = { alpha: { type: 'string', short: 'a' }, help: { type: 'boolean' } }; parseArgs({ args, options, enableHelpPrinting: true }); } finally { From eadccbbb128cfa41c60482609d49af66e32b1a64 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Thu, 3 Jul 2025 08:41:46 -0300 Subject: [PATCH 10/21] doc: update printUsage return type --- doc/api/util.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/util.md b/doc/api/util.md index 0ed00560e91dec..0db7644c4dc250 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -2061,7 +2061,7 @@ changes: * `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 + * `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 From 2be17878963b83bd002e7b602bc1f4e7850665a3 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Mon, 7 Jul 2025 16:33:53 -0300 Subject: [PATCH 11/21] lib: remove enableHelpPrinting option --- doc/api/util.md | 37 +++++++++++++++------- lib/internal/util/parse_args/parse_args.js | 8 ++--- test/parallel/test-parse-args.mjs | 25 ++++----------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 0db7644c4dc250..19baf263f6fc34 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -2051,9 +2051,6 @@ changes: 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 and a `--help` entry was provided, 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} @@ -2110,9 +2107,11 @@ console.log(values, positionals); ### `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. +`parseArgs` supports automatic help text generation for command-line options. To use this feature, add a `help` +property to each option and optionally provide general help text using the `help` config property. + +When both general help text is provided and `--help` is present in the `args`, `parseArgs` +will automatically print the help message and exit with code 0. ```mjs import { parseArgs } from 'node:util'; @@ -2152,12 +2151,20 @@ if (result.printUsage) { } // Or automatically print help and exit +const args = ['-h']; parseArgs({ + args, options, help: 'My CLI Tool v1.0\n\nProcess files with various options.', - enableHelpPrinting: true, }); -// Prints help and exits with code 0 +// Prints: +// My CLI Tool v1.0 +// +// Process files with various options. +// -v, --verbose Enable verbose output +// -h, --help. Prints command line options +// --output Output directory +// exit with code 0 ``` ```cjs @@ -2180,7 +2187,7 @@ const options = { }, }; -// Get help text in result +// Get serialized help text in result const result = parseArgs({ options, help: 'My CLI Tool v1.0\n\nProcess files with various options.', @@ -2198,12 +2205,20 @@ if (result.printUsage) { } // Or automatically print help and exit +const args = ['-h']; parseArgs({ + args, options, help: 'My CLI Tool v1.0\n\nProcess files with various options.', - enableHelpPrinting: true, }); -// Prints help and exits with code 0 +// Prints: +// My CLI Tool v1.0 +// +// Process files with various options. +// -v, --verbose Enable verbose output +// -h, --help. Prints command line options +// --output Output directory +// exit with code 0 ``` ### `parseArgs` `tokens` diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index c451e37b7b005d..f2cfccef564c2c 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -350,7 +350,6 @@ const parseArgs = (config = kEmptyObject) => { 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 }; @@ -362,7 +361,6 @@ const parseArgs = (config = kEmptyObject) => { validateBoolean(allowNegative, 'allowNegative'); validateObject(options, 'options'); validateString(help, 'help'); - validateBoolean(enableHelpPrinting, 'enableHelpPrinting'); ArrayPrototypeForEach( ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { @@ -466,12 +464,10 @@ const parseArgs = (config = kEmptyObject) => { }); const helpRequested = result.values.help; - if (enableHelpPrinting && helpRequested) { + if (help && helpRequested) { const console = require('internal/console/global'); - if (printUsage.length > 0 || help) { + if (printUsage.length > 0) { console.log(printUsage); - } else { - console.log('No help text available.'); } process.exit(0); } else if (printUsage.length > 0) { diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index a7ce95c307a7a4..1abe0232efdeba 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1138,17 +1138,6 @@ test('when help value is added, then add initial help text', () => { assert.deepStrictEqual(result, expected); }); -test('enableHelpPrinting config must be a boolean', () => { - const args = ['-f', 'bar']; - const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; - const help = 'Description for some awesome stuff:'; - const enableHelpPrinting = 'not a boolean'; - assert.throws(() => { - parseArgs({ args, options, help, enableHelpPrinting }); - }, /The "enableHelpPrinting" argument must be of type boolean/ - ); -}); - function setupConsoleAndExit() { const originalLog = console.log; const originalExit = process.exit; @@ -1172,13 +1161,12 @@ function setupConsoleAndExit() { return { getOutput: () => output, getExitCode: () => exitCode, restore }; } -test('when enableHelpPrinting config is true, print all help text and exit', () => { +test('when --help flag is present with help arg, prints all help text and exit', () => { const { getOutput, getExitCode, restore } = setupConsoleAndExit(); try { const args = [ - '-h', '-a', 'val1', '--beta', '-c', 'val3', '--delta', 'val4', '-e', - '--foxtrot', 'val6', '--golf', '--hotel', 'val8', '--india', 'val9', '-j', + '-h', '-a', 'val1', ]; const options = { help: { type: 'boolean', short: 'h', help: 'Prints command line options' }, @@ -1200,7 +1188,7 @@ test('when enableHelpPrinting config is true, print all help text and exit', () }; const help = 'Description for some awesome stuff:'; - parseArgs({ args, options, help, enableHelpPrinting: true }); + parseArgs({ args, options, help }); } finally { restore(); } @@ -1223,18 +1211,19 @@ test('when enableHelpPrinting config is true, print all help text and exit', () assert.strictEqual(getOutput(), expectedOutput); }); -test('when enableHelpPrinting config is true, but no help text is available', () => { +test('when --help flag is present with help arg but no help text is available, prints help text and exit', () => { const { getOutput, getExitCode, restore } = setupConsoleAndExit(); try { const args = ['-a', 'val1', '--help']; + const help = 'Description for some awesome stuff:'; const options = { alpha: { type: 'string', short: 'a' }, help: { type: 'boolean' } }; - parseArgs({ args, options, enableHelpPrinting: true }); + parseArgs({ args, options, help }); } finally { restore(); } assert.strictEqual(getExitCode(), 0); - assert.strictEqual(getOutput(), 'No help text available.\n'); + assert.strictEqual(getOutput(), 'Description for some awesome stuff:\n'); }); From 6c363f5bdf3b1c24fc3e7e6931dc78cd85474cc8 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 12 Jul 2025 12:42:50 -0300 Subject: [PATCH 12/21] lib: refactor formatHelpTextForPrint and simplify printUsage logic --- doc/api/util.md | 52 +------ lib/internal/util/parse_args/parse_args.js | 35 ++--- test/parallel/test-parse-args.mjs | 171 ++++++++------------- 3 files changed, 88 insertions(+), 170 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 19baf263f6fc34..49c269571a5433 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1994,7 +1994,7 @@ changes: - version: - REPLACEME pr-url: https://github.com/nodejs/node/pull/58875 - description: Add support for help text in options and enableHelpPrinting config. + description: Add support for help text in options and general help text. - version: - v22.4.0 - v20.16.0 @@ -2058,8 +2058,8 @@ changes: * `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`. + * `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 @@ -2107,11 +2107,9 @@ console.log(values, positionals); ### `parseArgs` help text -`parseArgs` supports automatic help text generation for command-line options. To use this feature, add a `help` -property to each option and optionally provide general help text using the `help` config property. - -When both general help text is provided and `--help` is present in the `args`, `parseArgs` -will automatically print the help message and exit with code 0. +`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. ```mjs import { parseArgs } from 'node:util'; @@ -2120,7 +2118,6 @@ const options = { verbose: { type: 'boolean', short: 'v', - help: 'Enable verbose output', }, help: { type: 'boolean', @@ -2145,26 +2142,10 @@ if (result.printUsage) { // My CLI Tool v1.0 // // Process files with various options. - // -v, --verbose Enable verbose output + // -v, --verbose // -h, --help. Prints command line options // --output Output directory } - -// Or automatically print help and exit -const args = ['-h']; -parseArgs({ - args, - options, - help: 'My CLI Tool v1.0\n\nProcess files with various options.', -}); -// Prints: -// My CLI Tool v1.0 -// -// Process files with various options. -// -v, --verbose Enable verbose output -// -h, --help. Prints command line options -// --output Output directory -// exit with code 0 ``` ```cjs @@ -2174,7 +2155,6 @@ const options = { verbose: { type: 'boolean', short: 'v', - help: 'Enable verbose output', }, help: { type: 'boolean', @@ -2199,26 +2179,10 @@ if (result.printUsage) { // My CLI Tool v1.0 // // Process files with various options. - // -v, --verbose Enable verbose output + // -v, --verbose // -h, --help. Prints command line options // --output Output directory } - -// Or automatically print help and exit -const args = ['-h']; -parseArgs({ - args, - options, - help: 'My CLI Tool v1.0\n\nProcess files with various options.', -}); -// Prints: -// My CLI Tool v1.0 -// -// Process files with various options. -// -v, --verbose Enable verbose output -// -h, --help. Prints command line options -// --output Output directory -// exit with code 0 ``` ### `parseArgs` `tokens` diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index f2cfccef564c2c..6eef5319b60bdc 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -322,16 +322,16 @@ function formatHelpTextForPrint(longOption, optionConfig) { const help = objectGetOwn(optionConfig, 'help'); let helpTextForPrint = ''; + if (shortOption) { + helpTextForPrint += `-${shortOption}, `; + } + helpTextForPrint += `--${longOption}`; + if (type === 'string') { + helpTextForPrint += ' '; + } else if (type === 'boolean') { + helpTextForPrint += ''; + } if (help) { - if (shortOption) { - helpTextForPrint += `-${shortOption}, `; - } - helpTextForPrint += `--${longOption}`; - if (type === 'string') { - helpTextForPrint += ' '; - } else if (type === 'boolean') { - helpTextForPrint += ''; - } if (helpTextForPrint.length > layoutSpacing) { helpTextForPrint += '\n' + ''.padEnd(layoutSpacing) + help; } else { @@ -455,22 +455,13 @@ const parseArgs = (config = kEmptyObject) => { ArrayPrototypeForEach(ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { const helpTextForPrint = formatHelpTextForPrint(longOption, optionConfig); - if (helpTextForPrint) { - if (printUsage.length > 0) { - printUsage += '\n'; - } - printUsage += helpTextForPrint; + if (printUsage.length > 0) { + printUsage += '\n'; } + printUsage += helpTextForPrint; }); - const helpRequested = result.values.help; - if (help && helpRequested) { - const console = require('internal/console/global'); - if (printUsage.length > 0) { - console.log(printUsage); - } - process.exit(0); - } else if (printUsage.length > 0) { + if (help && printUsage.length > 0) { result.printUsage = printUsage; } diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 1abe0232efdeba..f46d8c99503c24 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1063,6 +1063,16 @@ test('auto-detect --no-foo as negated when strict:false and allowNegative', () = 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 } }; @@ -1072,158 +1082,111 @@ test('help value for option must be a string', () => { ); }); -test('when help value for lone short option is added, then add help text', () => { +test('when help arg with help value for lone short option is added, then add help text', () => { const args = ['-f', 'bar']; const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; - const printUsage = '-f, --foo help text'; + const help = 'Description for some awesome stuff:'; + const printUsage = help + '\n-f, --foo help text'; const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; - const result = parseArgs({ args, options, allowPositionals: true }); + const result = parseArgs({ args, options, allowPositionals: true, help }); assert.deepStrictEqual(result, expected); }); -test('when help value for short group option is added, then add help text', () => { +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 printUsage = '-f, --foo help text\n-m, --moo help text'; + const help = 'Description for some awesome stuff:'; + const printUsage = help + '\n-f, --foo help text\n-m, --moo help text'; const expected = { values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [], printUsage }; - const result = parseArgs({ args, options, allowPositionals: true }); + const result = parseArgs({ args, options, allowPositionals: true, help }); assert.deepStrictEqual(result, expected); }); -test('when help value for short option and value is added, then add help text', () => { +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 printUsage = '-f, --foo help text'; + const help = 'Description for some awesome stuff:'; + const printUsage = help + '\n-f, --foo help text'; const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [], printUsage }; - const result = parseArgs({ args, options, allowPositionals: true }); + const result = parseArgs({ args, options, allowPositionals: true, help }); assert.deepStrictEqual(result, expected); }); -test('when help value for lone long option is added, then add help text', () => { +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 printUsage = '--foo help text'; + const help = 'Description for some awesome stuff:'; + const printUsage = help + '\n--foo help text'; const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; - const result = parseArgs({ args, options, allowPositionals: true }); + const result = parseArgs({ args, options, allowPositionals: true, help }); assert.deepStrictEqual(result, expected); }); -test('when help value for lone long option and value is added, then add help text', () => { +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 printUsage = '--foo help text'; - const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; - const result = parseArgs({ args, options, allowPositionals: true }); - assert.deepStrictEqual(result, expected); -}); - -test('help 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('when help value is added, then add initial help text', () => { - 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 help text'; + const printUsage = help + '\n--foo help text'; const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; - const result = parseArgs({ args, options, help }); + const result = parseArgs({ args, options, allowPositionals: true, help }); assert.deepStrictEqual(result, expected); }); -function setupConsoleAndExit() { - const originalLog = console.log; - const originalExit = process.exit; - - let output = ''; - let exitCode = null; - - console.log = (message) => { - output += message + '\n'; - }; - - process.exit = (code) => { - exitCode = code; +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:'; - function restore() { - console.log = originalLog; - process.exit = originalExit; - } - - return { getOutput: () => output, getExitCode: () => exitCode, restore }; -} - -test('when --help flag is present with help arg, prints all help text and exit', () => { - const { getOutput, getExitCode, restore } = setupConsoleAndExit(); - - try { - 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:'; - - parseArgs({ args, options, help }); - } finally { - restore(); - } - - const expectedOutput = + const result = parseArgs({ args, options, help }); + const printUsage = 'Description for some awesome stuff:\n' + '-h, --help Prints command line options\n' + '-a, --alpha Alpha option help\n' + '-b, --beta Beta option help\n' + + '-c, --charlie \n' + '--delta Delta option help\n' + '-e, --echo Echo option help\n' + '--foxtrot Foxtrot option help\n' + '--golf Golf option help\n' + '--hotel Hotel option help\n' + + '--india \n' + '-j, --juliet Juliet option help\n' + '-L, --looooooooooooooongHelpText \n' + - ' Very long option help text for demonstration purposes\n'; + ' Very long option help text for demonstration purposes'; - assert.strictEqual(getExitCode(), 0); - assert.strictEqual(getOutput(), expectedOutput); + assert.strictEqual(result.printUsage, printUsage); }); -test('when --help flag is present with help arg but no help text is available, prints help text and exit', () => { - const { getOutput, getExitCode, restore } = setupConsoleAndExit(); - - try { - const args = ['-a', 'val1', '--help']; - const help = 'Description for some awesome stuff:'; - const options = { alpha: { type: 'string', short: 'a' }, help: { type: 'boolean' } }; +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 \n' + + '--help'; - parseArgs({ args, options, help }); - } finally { - restore(); - } + const result = parseArgs({ args, options, help }); - assert.strictEqual(getExitCode(), 0); - assert.strictEqual(getOutput(), 'Description for some awesome stuff:\n'); + assert.strictEqual(result.printUsage, printUsage); }); From 213c0618a2ad8c7189b6c8e45f6aec26b296adf4 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Filho Date: Sat, 26 Jul 2025 09:58:42 -0300 Subject: [PATCH 13/21] test: improve tests description Co-authored-by: John Gee --- test/parallel/test-parse-args.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index f46d8c99503c24..131a577f4453e7 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1082,7 +1082,7 @@ test('help value for option must be a string', () => { ); }); -test('when help arg with help value for lone short option is added, then add help text', () => { +test('when option has short and long flags, then both appear in usage', () => { const args = ['-f', 'bar']; const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; const help = 'Description for some awesome stuff:'; From e6a219f1079343e530ed29eaf69230bcf41f8318 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 26 Jul 2025 10:34:30 -0300 Subject: [PATCH 14/21] test: update return structure and improve tests descriptions --- doc/api/util.md | 6 ++-- lib/internal/util/parse_args/parse_args.js | 5 ++-- test/parallel/test-parse-args.mjs | 34 +++++++++++++--------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 49c269571a5433..5a0c7e662fe9a1 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -2055,11 +2055,11 @@ changes: * Returns: {Object} The parsed command line arguments: * `values` {Object} A mapping of parsed option names with their {string} or {boolean} values. + * `help` {string | undefined} Formatted help text for all options provided. Only included if general `help` text + is available. * `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 @@ -2109,7 +2109,7 @@ console.log(values, positionals); `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. +a `help` property to each option can be optionally included. ```mjs import { parseArgs } from 'node:util'; diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 6eef5319b60bdc..5224b767f84310 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -328,9 +328,8 @@ function formatHelpTextForPrint(longOption, optionConfig) { helpTextForPrint += `--${longOption}`; if (type === 'string') { helpTextForPrint += ' '; - } else if (type === 'boolean') { - helpTextForPrint += ''; } + if (help) { if (helpTextForPrint.length > layoutSpacing) { helpTextForPrint += '\n' + ''.padEnd(layoutSpacing) + help; @@ -462,7 +461,7 @@ const parseArgs = (config = kEmptyObject) => { }); if (help && printUsage.length > 0) { - result.printUsage = printUsage; + result.values.help = printUsage; } return result; diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 131a577f4453e7..5fb87f6e6d64fe 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1082,58 +1082,66 @@ test('help value for option must be a 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', () => { 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 help text'; - const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; + const expected = { values: { __proto__: null, foo: 'bar', help: printUsage }, positionals: [] }; 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', () => { +test('when options has short group flags, then both appear in usage', () => { 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 help text'; - const expected = { values: { __proto__: null, foo: true, moo: 'bar' }, positionals: [], printUsage }; + const expected = { values: { help: printUsage, __proto__: null, foo: true, moo: 'bar' }, positionals: [] }; 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', () => { +test('when options has short flag with value, then both appear in usage', () => { 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 help text'; - const expected = { values: { __proto__: null, foo: 'FILE' }, positionals: [], printUsage }; + const expected = { values: { help: printUsage, __proto__: null, foo: 'FILE' }, positionals: [] }; 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', () => { +test('when options has long flag, then it appear in usage', () => { const args = ['--foo', 'bar']; const options = { foo: { type: 'string', help: 'help text' } }; const help = 'Description for some awesome stuff:'; const printUsage = help + '\n--foo help text'; - const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; + const expected = { values: { help: printUsage, __proto__: null, foo: 'bar' }, positionals: [] }; 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', () => { +test('when options has long flag with value, then both appear in usage', () => { const args = ['--foo=bar']; const options = { foo: { type: 'string', help: 'help text' } }; const help = 'Description for some awesome stuff:'; const printUsage = help + '\n--foo help text'; - const expected = { values: { __proto__: null, foo: 'bar' }, positionals: [], printUsage }; + const expected = { values: { help: printUsage, __proto__: null, foo: 'bar' }, positionals: [] }; 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', () => { +test('when options has help values with and without explicit texts, then all appear in usage', () => { const args = [ '-h', '-a', 'val1', ]; @@ -1174,10 +1182,10 @@ test('when help arg with help values and without explicit help texts, then add h '-L, --looooooooooooooongHelpText \n' + ' Very long option help text for demonstration purposes'; - assert.strictEqual(result.printUsage, printUsage); + assert.strictEqual(result.values.help, printUsage); }); -test('when help arg but no help text is available, then add help text', () => { +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' } }; @@ -1188,5 +1196,5 @@ test('when help arg but no help text is available, then add help text', () => { const result = parseArgs({ args, options, help }); - assert.strictEqual(result.printUsage, printUsage); + assert.strictEqual(result.values.help, printUsage); }); From c8923dd388879428ca067211cc3433f4993c074b Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 26 Jul 2025 10:57:25 -0300 Subject: [PATCH 15/21] test: fix lint --- test/parallel/test-parse-args.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 5fb87f6e6d64fe..219619901d99ba 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1084,11 +1084,11 @@ test('help value for option must be a 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 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', () => { const args = ['-f', 'bar']; From 40256fc7f9e404c5b5a113bcc666554f30f1c76f Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Filho Date: Sat, 30 Aug 2025 12:16:28 -0300 Subject: [PATCH 16/21] Update doc/api/util.md Co-authored-by: Jordan Harband --- doc/api/util.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/util.md b/doc/api/util.md index 5a0c7e662fe9a1..b97801e3f093a4 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -2122,7 +2122,7 @@ const options = { help: { type: 'boolean', short: 'h', - help: 'Prints command line options', + help: 'Prints usage information', }, output: { type: 'string', From 15b44d2cdfe3917c6975a6a46c0d16e204a06a0b Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 30 Aug 2025 12:23:29 -0300 Subject: [PATCH 17/21] lib: improve formatting in formatHelpTextForPrint using StringPrototypePadEnd --- lib/internal/util/parse_args/parse_args.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 5224b767f84310..e5cc86d3d52ae2 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -13,6 +13,7 @@ const { ObjectPrototypeHasOwnProperty: ObjectHasOwn, StringPrototypeCharAt, StringPrototypeIndexOf, + StringPrototypePadEnd, StringPrototypeSlice, StringPrototypeStartsWith, } = primordials; @@ -332,9 +333,9 @@ function formatHelpTextForPrint(longOption, optionConfig) { if (help) { if (helpTextForPrint.length > layoutSpacing) { - helpTextForPrint += '\n' + ''.padEnd(layoutSpacing) + help; + helpTextForPrint += `\n${StringPrototypePadEnd('', layoutSpacing)}${help}`; } else { - helpTextForPrint = helpTextForPrint.padEnd(layoutSpacing) + help; + helpTextForPrint = `${StringPrototypePadEnd(helpTextForPrint, layoutSpacing)}${help}`; } } From 55b29d55c1fc3a5dce234ea4fb8fed4c42cf3233 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 7 Sep 2025 12:21:45 -0300 Subject: [PATCH 18/21] lib: add support for auto-injecting help option and return help text in parseArgs --- lib/internal/util/parse_args/parse_args.js | 49 +++++++++++++++------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index e5cc86d3d52ae2..4f7da58cbca4dc 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -348,8 +348,25 @@ const parseArgs = (config = kEmptyObject) => { 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; + const addHelpOption = objectGetOwn(config, 'addHelpOption') ?? hasGenerateHelp; + const returnHelpText = objectGetOwn(config, 'returnHelpText') ?? (hasGenerateHelp || addHelpOption); + + // Auto-inject help option if requested and not already present + if (addHelpOption && !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 }; @@ -361,6 +378,8 @@ const parseArgs = (config = kEmptyObject) => { validateBoolean(allowNegative, 'allowNegative'); validateObject(options, 'options'); validateString(help, 'help'); + validateBoolean(addHelpOption, 'addHelpOption'); + validateBoolean(returnHelpText, 'returnHelpText'); ArrayPrototypeForEach( ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { @@ -447,22 +466,24 @@ 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); + // Phase 4: generate print usage for each option if requested + if (returnHelpText) { + 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 (printUsage.length > 0) { - printUsage += '\n'; + result.helpText = printUsage; } - printUsage += helpTextForPrint; - }); - - if (help && printUsage.length > 0) { - result.values.help = printUsage; } return result; From f8d9140ca027400e41fa14e0527bb8b8e6c8f36f Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 7 Sep 2025 12:22:37 -0300 Subject: [PATCH 19/21] test: enhance help option tests --- test/parallel/test-parse-args.mjs | 377 ++++++++++++++++++++---------- 1 file changed, 253 insertions(+), 124 deletions(-) diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 219619901d99ba..22c3a36c9ea851 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1063,138 +1063,267 @@ test('auto-detect --no-foo as negated when strict:false and allowNegative', () = 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 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('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 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', () => { - 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 help text'; - const expected = { values: { __proto__: null, foo: 'bar', help: printUsage }, positionals: [] }; - const result = parseArgs({ args, options, allowPositionals: true, help }); - 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 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', () => { - 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 help text'; - const expected = { values: { help: printUsage, __proto__: null, foo: true, moo: '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 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', () => { - 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 help text'; - const expected = { values: { help: printUsage, __proto__: null, foo: 'FILE' }, 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 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', () => { - const args = ['--foo', 'bar']; - const options = { foo: { type: 'string', help: 'help text' } }; - const help = 'Description for some awesome stuff:'; - const printUsage = help + '\n--foo help text'; - const expected = { values: { help: printUsage, __proto__: null, foo: 'bar' }, 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 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', () => { - const args = ['--foo=bar']; - const options = { foo: { type: 'string', help: 'help text' } }; - const help = 'Description for some awesome stuff:'; - const printUsage = help + '\n--foo help text'; - const expected = { values: { help: printUsage, __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 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 Alpha option help\n' + - '-b, --beta Beta option help\n' + - '-c, --charlie \n' + - '--delta Delta option help\n' + - '-e, --echo Echo option help\n' + - '--foxtrot Foxtrot option help\n' + - '--golf Golf option help\n' + - '--hotel Hotel option help\n' + - '--india \n' + - '-j, --juliet Juliet option help\n' + - '-L, --looooooooooooooongHelpText \n' + - ' Very long option help text for demonstration purposes'; - - assert.strictEqual(result.values.help, 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 = + 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' + - '-a, --alpha \n' + - '--help'; + '-h, --help Prints command line options\n' + + '-a, --alpha Alpha option help\n' + + '-b, --beta Beta option help\n' + + '-c, --charlie \n' + + '--delta Delta option help\n' + + '-e, --echo Echo option help\n' + + '--foxtrot Foxtrot option help\n' + + '--golf Golf option help\n' + + '--hotel Hotel option help\n' + + '--india \n' + + '-j, --juliet Juliet option help\n' + + '-L, --looooooooooooooongHelpText \n' + + ' Very long option help text for demonstration purposes'; + + assert.strictEqual(result.helpText, printUsage); + }); - const result = parseArgs({ args, options, help }); + 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 \n' + + '--help'; - assert.strictEqual(result.values.help, printUsage); -}); + const result = parseArgs({ args, options, help }); + + assert.strictEqual(result.helpText, printUsage); + }); + + // Test addHelpOption behavior + test('addHelpOption validation must be boolean', () => { + const args = ['-f', 'bar']; + const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; + + assert.throws(() => { + parseArgs({ args, options, addHelpOption: 'true' }); + }, /The "addHelpOption" argument must be of type boolean/ + ); + }); + + test('addHelpOption is true 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, addHelpOption: true }); + + assert.ok(result.helpText.includes('-h, --help Show help')); + const resultWithHelp = parseArgs({ args: ['--help'], options, help, addHelpOption: true }); + assert.strictEqual(resultWithHelp.values.help, true); + }); + + test('addHelpOption is false prevents auto-injection of 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, addHelpOption: false }); + + assert.ok(!result.helpText.includes('-h, --help')); + assert.throws(() => { + parseArgs({ args: ['--help'], options, help, addHelpOption: false }); + }, { code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' }); + }); + + test('addHelpOption is false but existing help option should still work', () => { + const args = ['--help']; + const options = { + foo: { type: 'string', help: 'use the foo filter' }, + help: { type: 'boolean', short: '?', help: 'display help' } + }; + const help = 'utility to control filters'; + + const result = parseArgs({ args, options, help, addHelpOption: false }); + + assert.strictEqual(result.values.help, true); + assert.ok(result.helpText.includes('-?, --help display help')); + }); + + // Test returnHelpText behavior + test('returnHelpText validation must be boolean', () => { + const args = ['-f', 'bar']; + const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; + + assert.throws(() => { + parseArgs({ args, options, returnHelpText: 'true' }); + }, /The "returnHelpText" argument must be of type boolean/ + ); + }); + + test('returnHelpText is false prevents help text generation', () => { + 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, returnHelpText: false }); + + assert.strictEqual(result.helpText, undefined); + }); + + test('returnHelpText is true forces help text generation even without general help', () => { + const args = ['--foo', 'bar']; + const options = { foo: { type: 'string', help: 'use the foo filter' } }; + + const result = parseArgs({ args, options, returnHelpText: true, addHelpOption: true }); + + assert.ok(result.helpText); + assert.ok(result.helpText.includes('--foo use the foo filter')); + assert.ok(result.helpText.includes('-h, --help Show help')); + }); + + test('postpone help generation until needed', () => { + const options = { + foo: { type: 'boolean', short: 'f', help: 'use the foo filter' }, + bar: { type: 'string', help: 'use the specified bar filter' } + }; + + const result = parseArgs({ + addHelpOption: true, + returnHelpText: false, + options + }); + + assert.strictEqual(result.helpText, undefined); + + const { helpText } = parseArgs({ + help: 'utility to control filters', + options + }); + + assert.ok(helpText); + assert.ok(helpText.includes('utility to control filters')); + }); + + test('when both addHelpOption and returnHelpText are false, no help functionality', () => { + const args = ['--foo', 'bar']; + const options = { foo: { type: 'string', help: 'help text' } }; + const help = 'Description text'; + + const result = parseArgs({ + args, + options, + help, + addHelpOption: false, + returnHelpText: false + }); + + assert.strictEqual(result.helpText, undefined); + assert.throws(() => { + parseArgs({ args: ['--help'], options, help, addHelpOption: false }); + }, { code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' }); + }); +} From caf28404fa79f0876c7dc9af9dccfb6bac1b1d9b Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 7 Sep 2025 12:31:01 -0300 Subject: [PATCH 20/21] doc: update util to enhance parseArgs help option functionality --- doc/api/util.md | 157 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 114 insertions(+), 43 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index b97801e3f093a4..485f61b74cd3cc 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -2051,13 +2051,19 @@ changes: the tokens in different ways. **Default:** `false`. * `help` {string} General help text to display at the beginning of help output. + * `addHelpOption` {boolean} Whether to automatically add a help option to the + options if general help text is provided and no existing help option is defined. + The auto-added help option uses `-h` short flag and `--help` long flag. + **Default:** `true` if `help` is provided, `false` otherwise. + * `returnHelpText` {boolean} Whether to return formatted help text in the result. + **Default:** `true` if `help` is provided or `addHelpOption` is `true`, `false` otherwise. * Returns: {Object} The parsed command line arguments: * `values` {Object} A mapping of parsed option names with their {string} or {boolean} values. - * `help` {string | undefined} Formatted help text for all options provided. Only included if general `help` text - is available. * `positionals` {string\[]} Positional arguments. + * `helpText` {string | undefined} Formatted help text for all options provided. + Only included if `returnHelpText` is `true`. * `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens) section. Only returned if `config` includes `tokens: true`. @@ -2107,81 +2113,146 @@ console.log(values, positionals); ### `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 -a `help` property to each option can be optionally included. +`parseArgs` supports automatic formatted help text generation for command-line options. When +general help text is provided, a help option is automatically added unless disabled or 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 = { - verbose: { - type: 'boolean', - short: 'v', - }, - help: { + foo: { type: 'boolean', - short: 'h', - help: 'Prints usage information', + short: 'f', + help: 'use the foo filter', }, - output: { + bar: { type: 'string', - help: 'Output directory', + help: 'use the specified bar filter', }, }; -// Get serialized help text in result const result = parseArgs({ + help: 'utility to control filters', options, - help: 'My CLI Tool v1.0\n\nProcess files with various options.', }); -if (result.printUsage) { - console.log(result.printUsage); +if (result.values.help) { + console.log(result.helpText); // Prints: - // My CLI Tool v1.0 - // - // Process files with various options. - // -v, --verbose - // -h, --help. Prints command line options - // --output Output directory + // utility to control filters + // -f, --foo use the foo filter + // --bar use the specified bar filter + // -h, --help Show help } ``` -```cjs -const { parseArgs } = require('node:util'); +#### Custom help option + +You can override the auto-added help option by defining your own: + +```mjs +import { parseArgs } from 'node:util'; const options = { - verbose: { + foo: { type: 'boolean', - short: 'v', + short: 'f', + help: 'use the foo filter', + }, + bar: { + type: 'string', + help: 'use the specified bar filter', }, help: { type: 'boolean', - short: 'h', - help: 'Prints command line options', + short: '?', + help: 'display help', }, - output: { - type: 'string', - help: 'Output directory', +}; + +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 use the specified bar filter + // -?, --help display help +} +``` + +#### Advanced configuration + +Use `addHelpOption` and `returnHelpText` to customize help behavior: + +```mjs +import { parseArgs } from 'node:util'; + +const options = { + foo: { + type: 'boolean', + short: 'f', + help: 'use the foo filter', + }, + assist: { + type: 'boolean', + short: '?', + help: 'display help', }, }; -// Get serialized help text in result const result = parseArgs({ + addHelpOption: false, // Suppress auto help option + returnHelpText: true, // Generate help text anyway options, - help: 'My CLI Tool v1.0\n\nProcess files with various options.', }); -if (result.printUsage) { - console.log(result.printUsage); +if (result.values.assist) { + console.log(result.helpText); // Prints: - // My CLI Tool v1.0 - // - // Process files with various options. - // -v, --verbose - // -h, --help. Prints command line options - // --output Output directory + // -f, --foo use the foo filter + // -?, --assist display help +} +``` + +#### Deferred help generation + +For performance, you can postpone help text generation until needed: + +```mjs +import { parseArgs } from 'node:util'; + +const options = { + foo: { + type: 'boolean', + short: 'f', + help: 'use the foo filter', + }, +}; + +// First parse without generating help text +const result = parseArgs({ + addHelpOption: true, + returnHelpText: false, + options, +}); + +if (result.values.help) { + // Generate help text only when needed + const { helpText } = parseArgs({ + help: 'utility to control filters', + options, + }); + console.log(helpText); } ``` From f6b6199c60980521a8326c832af797cc36d60d28 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sun, 2 Nov 2025 11:31:08 -0300 Subject: [PATCH 21/21] lib: removing returnHelpText and addHelpOption --- doc/api/util.md | 75 +------------ lib/internal/util/parse_args/parse_args.js | 36 +++---- test/parallel/test-parse-args.mjs | 118 +-------------------- 3 files changed, 18 insertions(+), 211 deletions(-) diff --git a/doc/api/util.md b/doc/api/util.md index 485f61b74cd3cc..1ef7ca7f2e8cf9 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -2051,19 +2051,12 @@ changes: the tokens in different ways. **Default:** `false`. * `help` {string} General help text to display at the beginning of help output. - * `addHelpOption` {boolean} Whether to automatically add a help option to the - options if general help text is provided and no existing help option is defined. - The auto-added help option uses `-h` short flag and `--help` long flag. - **Default:** `true` if `help` is provided, `false` otherwise. - * `returnHelpText` {boolean} Whether to return formatted help text in the result. - **Default:** `true` if `help` is provided or `addHelpOption` is `true`, `false` otherwise. * 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. - Only included if `returnHelpText` is `true`. * `tokens` {Object\[] | undefined} See [parseArgs tokens](#parseargs-tokens) section. Only returned if `config` includes `tokens: true`. @@ -2114,7 +2107,7 @@ console.log(values, positionals); ### `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 disabled or already present. +general help text is provided, a help option is automatically added unless already present. #### Simple usage @@ -2190,72 +2183,6 @@ if (result.values.help) { } ``` -#### Advanced configuration - -Use `addHelpOption` and `returnHelpText` to customize help behavior: - -```mjs -import { parseArgs } from 'node:util'; - -const options = { - foo: { - type: 'boolean', - short: 'f', - help: 'use the foo filter', - }, - assist: { - type: 'boolean', - short: '?', - help: 'display help', - }, -}; - -const result = parseArgs({ - addHelpOption: false, // Suppress auto help option - returnHelpText: true, // Generate help text anyway - options, -}); - -if (result.values.assist) { - console.log(result.helpText); - // Prints: - // -f, --foo use the foo filter - // -?, --assist display help -} -``` - -#### Deferred help generation - -For performance, you can postpone help text generation until needed: - -```mjs -import { parseArgs } from 'node:util'; - -const options = { - foo: { - type: 'boolean', - short: 'f', - help: 'use the foo filter', - }, -}; - -// First parse without generating help text -const result = parseArgs({ - addHelpOption: true, - returnHelpText: false, - options, -}); - -if (result.values.help) { - // Generate help text only when needed - const { helpText } = parseArgs({ - help: 'utility to control filters', - options, - }); - console.log(helpText); -} -``` - ### `parseArgs` `tokens` Detailed parse information is available for adding custom behaviors by diff --git a/lib/internal/util/parse_args/parse_args.js b/lib/internal/util/parse_args/parse_args.js index 4f7da58cbca4dc..faf42df1d3b294 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -352,11 +352,7 @@ const parseArgs = (config = kEmptyObject) => { const help = objectGetOwn(config, 'help') ?? ''; const hasGenerateHelp = help.length > 0; - const addHelpOption = objectGetOwn(config, 'addHelpOption') ?? hasGenerateHelp; - const returnHelpText = objectGetOwn(config, 'returnHelpText') ?? (hasGenerateHelp || addHelpOption); - - // Auto-inject help option if requested and not already present - if (addHelpOption && !ObjectHasOwn(options, 'help')) { + if (hasGenerateHelp && !ObjectHasOwn(options, 'help')) { options = { ...options, __proto__: null, @@ -378,8 +374,6 @@ const parseArgs = (config = kEmptyObject) => { validateBoolean(allowNegative, 'allowNegative'); validateObject(options, 'options'); validateString(help, 'help'); - validateBoolean(addHelpOption, 'addHelpOption'); - validateBoolean(returnHelpText, 'returnHelpText'); ArrayPrototypeForEach( ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { @@ -466,24 +460,22 @@ const parseArgs = (config = kEmptyObject) => { } }); - // Phase 4: generate print usage for each option if requested - if (returnHelpText) { - 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; - }); + // 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) { - result.helpText = printUsage; + printUsage += '\n'; } + printUsage += helpTextForPrint; + }); + + if (help && printUsage.length > 0) { + result.helpText = printUsage; } return result; diff --git a/test/parallel/test-parse-args.mjs b/test/parallel/test-parse-args.mjs index 22c3a36c9ea851..f040b546d230d0 100644 --- a/test/parallel/test-parse-args.mjs +++ b/test/parallel/test-parse-args.mjs @@ -1203,127 +1203,15 @@ test('auto-detect --no-foo as negated when strict:false and allowNegative', () = assert.strictEqual(result.helpText, printUsage); }); - // Test addHelpOption behavior - test('addHelpOption validation must be boolean', () => { - const args = ['-f', 'bar']; - const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; - - assert.throws(() => { - parseArgs({ args, options, addHelpOption: 'true' }); - }, /The "addHelpOption" argument must be of type boolean/ - ); - }); - - test('addHelpOption is true auto-injects help option when no existing help option', () => { + 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, addHelpOption: true }); + const result = parseArgs({ args, options, help }); assert.ok(result.helpText.includes('-h, --help Show help')); - const resultWithHelp = parseArgs({ args: ['--help'], options, help, addHelpOption: true }); + const resultWithHelp = parseArgs({ args: ['--help'], options, help }); assert.strictEqual(resultWithHelp.values.help, true); }); - - test('addHelpOption is false prevents auto-injection of 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, addHelpOption: false }); - - assert.ok(!result.helpText.includes('-h, --help')); - assert.throws(() => { - parseArgs({ args: ['--help'], options, help, addHelpOption: false }); - }, { code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' }); - }); - - test('addHelpOption is false but existing help option should still work', () => { - const args = ['--help']; - const options = { - foo: { type: 'string', help: 'use the foo filter' }, - help: { type: 'boolean', short: '?', help: 'display help' } - }; - const help = 'utility to control filters'; - - const result = parseArgs({ args, options, help, addHelpOption: false }); - - assert.strictEqual(result.values.help, true); - assert.ok(result.helpText.includes('-?, --help display help')); - }); - - // Test returnHelpText behavior - test('returnHelpText validation must be boolean', () => { - const args = ['-f', 'bar']; - const options = { foo: { type: 'string', short: 'f', help: 'help text' } }; - - assert.throws(() => { - parseArgs({ args, options, returnHelpText: 'true' }); - }, /The "returnHelpText" argument must be of type boolean/ - ); - }); - - test('returnHelpText is false prevents help text generation', () => { - 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, returnHelpText: false }); - - assert.strictEqual(result.helpText, undefined); - }); - - test('returnHelpText is true forces help text generation even without general help', () => { - const args = ['--foo', 'bar']; - const options = { foo: { type: 'string', help: 'use the foo filter' } }; - - const result = parseArgs({ args, options, returnHelpText: true, addHelpOption: true }); - - assert.ok(result.helpText); - assert.ok(result.helpText.includes('--foo use the foo filter')); - assert.ok(result.helpText.includes('-h, --help Show help')); - }); - - test('postpone help generation until needed', () => { - const options = { - foo: { type: 'boolean', short: 'f', help: 'use the foo filter' }, - bar: { type: 'string', help: 'use the specified bar filter' } - }; - - const result = parseArgs({ - addHelpOption: true, - returnHelpText: false, - options - }); - - assert.strictEqual(result.helpText, undefined); - - const { helpText } = parseArgs({ - help: 'utility to control filters', - options - }); - - assert.ok(helpText); - assert.ok(helpText.includes('utility to control filters')); - }); - - test('when both addHelpOption and returnHelpText are false, no help functionality', () => { - const args = ['--foo', 'bar']; - const options = { foo: { type: 'string', help: 'help text' } }; - const help = 'Description text'; - - const result = parseArgs({ - args, - options, - help, - addHelpOption: false, - returnHelpText: false - }); - - assert.strictEqual(result.helpText, undefined); - assert.throws(() => { - parseArgs({ args: ['--help'], options, help, addHelpOption: false }); - }, { code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION' }); - }); }