From 7e00ac1eb3edd6aa8b47efb6e6d47fea668c205b Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Fri, 27 Jun 2025 21:40:20 -0300 Subject: [PATCH 1/8] 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 09b3ff7316d3ef..4db76e5fff8820 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, @@ -218,6 +219,7 @@ function argsToTokens(args, options) { // e.g. '-f' const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); + const helpValue = findHelpValueForOption(longOption, options); let value; let inlineValue; if (optionsGetOwn(options, longOption, 'type') === 'string' && @@ -229,7 +231,7 @@ function argsToTokens(args, options) { ArrayPrototypePush( tokens, { kind: 'option', name: longOption, rawName: arg, - index, value, inlineValue }); + index, value, inlineValue, ...(helpValue !== undefined && { help: helpValue }) }); if (value != null) ++index; continue; } @@ -261,16 +263,19 @@ function argsToTokens(args, options) { const shortOption = StringPrototypeCharAt(arg, 1); const longOption = findLongOptionForShort(shortOption, options); const value = StringPrototypeSlice(arg, 2); + const helpValue = findHelpValueForOption(longOption, options); ArrayPrototypePush( tokens, { kind: 'option', name: longOption, rawName: `-${shortOption}`, - index, value, inlineValue: true }); + index, value, inlineValue: true, + ...(helpValue !== undefined && { help: helpValue }) }); continue; } if (isLoneLongOption(arg)) { // e.g. '--foo' const longOption = StringPrototypeSlice(arg, 2); + const helpValue = findHelpValueForOption(longOption, options); let value; let inlineValue; if (optionsGetOwn(options, longOption, 'type') === 'string' && @@ -282,7 +287,7 @@ function argsToTokens(args, options) { ArrayPrototypePush( tokens, { kind: 'option', name: longOption, rawName: arg, - index, value, inlineValue }); + index, value, inlineValue, ...(helpValue !== undefined && { help: helpValue }) }); if (value != null) ++index; continue; } @@ -292,10 +297,11 @@ function argsToTokens(args, options) { const equalIndex = StringPrototypeIndexOf(arg, '='); const longOption = StringPrototypeSlice(arg, 2, equalIndex); const value = StringPrototypeSlice(arg, equalIndex + 1); + const helpValue = findHelpValueForOption(longOption, options); ArrayPrototypePush( tokens, { kind: 'option', name: longOption, rawName: `--${longOption}`, - index, value, inlineValue: true }); + index, value, inlineValue: true, ...(helpValue !== undefined && { help: helpValue }) }); continue; } @@ -325,7 +331,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']); @@ -361,6 +366,11 @@ const parseArgs = (config = kEmptyObject) => { } validator(defaultValue, `options.${longOption}.default`); } + + const helpOption = objectGetOwn(optionConfig, 'help'); + if (ObjectHasOwn(optionConfig, 'help')) { + validateString(helpOption, `options.${longOption}.help`); + } }, ); @@ -403,6 +413,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 95f787daf1debf..a17c834e6328f0 100644 --- a/lib/internal/util/parse_args/utils.js +++ b/lib/internal/util/parse_args/utils.js @@ -170,6 +170,21 @@ function findLongOptionForShort(shortOption, options) { return longOptionEntry?.[0] ?? shortOption; } +/** + * Find the help value associated with a long option. + * @param {string} longOption + * @param {object} options + * @returns {string|undefined} - the help value or undefined if not found + */ +function findHelpValueForOption(longOption, options) { + validateObject(options, 'options'); + if (ObjectHasOwn(options, longOption)) { + return objectGetOwn(options[longOption], 'help'); + } + + return undefined; +} + /** * Check if the given option includes a default value * and that option has not been set by the input args. @@ -183,6 +198,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 f801a61ac93c9de426549f100d67d9eaaf669d4d Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 12:59:29 -0300 Subject: [PATCH 2/8] 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 4db76e5fff8820..dd106b7225d40c 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -310,6 +310,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; @@ -413,33 +444,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 63f7af505b9ae47457e8a0d037dbd3ddb2c81c77 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 13:32:09 -0300 Subject: [PATCH 3/8] 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 dd106b7225d40c..b7aa1fb3bea66d 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -313,7 +313,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' }) @@ -348,6 +348,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 }; @@ -358,6 +359,7 @@ const parseArgs = (config = kEmptyObject) => { validateBoolean(returnTokens, 'tokens'); validateBoolean(allowNegative, 'allowNegative'); validateObject(options, 'options'); + validateString(help, 'help'); ArrayPrototypeForEach( ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { @@ -446,6 +448,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 a176401477827280649d4911b63a66c90230434f Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:15:33 -0300 Subject: [PATCH 4/8] 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 b7aa1fb3bea66d..cfddd45fa0f502 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -320,6 +320,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'); @@ -335,7 +337,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; @@ -349,6 +355,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 }; @@ -360,6 +367,7 @@ const parseArgs = (config = kEmptyObject) => { validateBoolean(allowNegative, 'allowNegative'); validateObject(options, 'options'); validateString(help, 'help'); + validateBoolean(enableHelpPrinting, 'enableHelpPrinting'); ArrayPrototypeForEach( ObjectEntries(options), ({ 0: longOption, 1: optionConfig }) => { @@ -459,7 +467,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 c0f677761f7b9d875371b238c34cb9d0a9d1ff17 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:36:16 -0300 Subject: [PATCH 5/8] 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 cfddd45fa0f502..65fd44c245ed05 100644 --- a/lib/internal/util/parse_args/parse_args.js +++ b/lib/internal/util/parse_args/parse_args.js @@ -467,12 +467,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 469d86ed2a73384d0715fa5dba61fd02fac586ff Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:36:31 -0300 Subject: [PATCH 6/8] 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 324c6ec1a799718e310fce75108fe082e8e4b488 Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:40:23 -0300 Subject: [PATCH 7/8] 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 6099f6a7b8d59a..3d252337b905b4 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1921,6 +1921,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 @@ -1959,6 +1963,7 @@ changes: be used if (and only if) the option does not appear in the arguments to be parsed. It must be of the same type as the `type` property. When `multiple` is `true`, it must be an array. + * `help` {string} Descriptive text to display in help output for this option. * `strict` {boolean} Should an error be thrown when unknown arguments are encountered, or when arguments are passed that do not match the `type` configured in `options`. @@ -1973,6 +1978,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} @@ -1980,6 +1989,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 @@ -2025,6 +2036,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 aab60e8a9812f66570c6e6bcfae4addefd1cc7aa Mon Sep 17 00:00:00 2001 From: Miguel Marcondes Date: Sat, 28 Jun 2025 15:51:38 -0300 Subject: [PATCH 8/8] 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 3d252337b905b4..006695566169db 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -1923,7 +1923,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 @@ -2075,8 +2075,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 @@ -2121,8 +2121,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