diff --git a/lib/internal/main/print_help.js b/lib/internal/main/print_help.js index 62269956be9faf..d788c823fb2c13 100644 --- a/lib/internal/main/print_help.js +++ b/lib/internal/main/print_help.js @@ -25,11 +25,16 @@ const { } = require('internal/process/pre_execution'); const { getCLIOptionsInfo, getOptionValue } = require('internal/options'); +const { styleText } = require('util'); const typeLookup = []; for (const key of ObjectKeys(types)) typeLookup[types[key]] = key; +function style(format, text) { + return styleText(format, text, { stream: process.stdout }); +} + // Environment variables are parsed ad-hoc throughout the code base, // so we gather the documentation here. const { hasIntl, hasSmallICU, hasNodeOptions } = internalBinding('config'); @@ -117,7 +122,7 @@ function getArgDescription(type) { } function format( - { options, aliases = new SafeMap(), firstColumn, secondColumn }, + { options, aliases = new SafeMap(), firstColumn, secondColumn, nameStyle = [] }, ) { let text = ''; let maxFirstColumnUsed = 0; @@ -176,7 +181,7 @@ function format( displayHelpText += ' (currently set)'; } - text += displayName; + text += style(nameStyle, displayName); maxFirstColumnUsed = MathMax(maxFirstColumnUsed, displayName.length); if (displayName.length >= firstColumn) text += '\n' + StringPrototypeRepeat(' ', firstColumn); @@ -194,6 +199,7 @@ function format( aliases, firstColumn: maxFirstColumnUsed + 2, secondColumn, + nameStyle, }); } @@ -214,20 +220,29 @@ function print(stream) { 'interactive mode if a tty)' }); options.set('--', { helpText: 'indicate the end of node options' }); let helpText = ( - 'Usage: node [options] [ script.js ] [arguments]\n' + - ' node inspect [options] [ script.js | host:port ] [arguments]\n\n' + - 'Options:\n'); + style('bold', + 'Usage: node [options] [ script.js ] [arguments]\n' + + ' node inspect [options] [ script.js | host:port ] [arguments]') + + '\n\n' + style('bold', 'Options:') + '\n'); helpText += (indent(format({ - options, aliases, firstColumn, secondColumn, + options, aliases, firstColumn, secondColumn, nameStyle: ['bold', 'green'], }), 2)); - helpText += ('\nEnvironment variables:\n'); + helpText += ('\n' + style('bold', 'Environment variables:') + '\n'); helpText += (format({ - options: envVars, firstColumn, secondColumn, + options: envVars, firstColumn, secondColumn, nameStyle: ['bold', 'magenta'], })); - helpText += ('\nDocumentation can be found at https://nodejs.org/'); + helpText += ('\nDocumentation can be found at ' + + style(['blue', 'underline'], 'https://nodejs.org/')); + + helpText = RegExpPrototypeSymbolReplace( + / \(currently set\)/g, + helpText, + style('dim', ' (currently set)'), + ); + console.log(helpText); } diff --git a/test/parallel/test-cli-node-print-help-style.js b/test/parallel/test-cli-node-print-help-style.js new file mode 100644 index 00000000000000..47fe7b75620a8d --- /dev/null +++ b/test/parallel/test-cli-node-print-help-style.js @@ -0,0 +1,64 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { execFile } = require('child_process'); + +// eslint-disable-next-line no-control-regex +const ANSI_SGR_REGEX = new RegExp('\x1b\\[[0-9;]*m', 'g'); + +function stripAnsi(text) { + return text.replace(ANSI_SGR_REGEX, ''); +} + +// Test: FORCE_COLOR=1 produces styled output +{ + const env = { ...process.env, FORCE_COLOR: '1' }; + execFile(process.execPath, ['--help'], { env }, + common.mustSucceed((stdout) => { + assert.ok(stdout.includes('\x1b[1m'), 'bold for headers should be present'); + assert.ok(stdout.includes('\x1b[32m'), 'green for option names should be present'); + assert.ok(stdout.includes('\x1b[35m'), 'magenta for env var names should be present'); + assert.ok(stdout.includes('\x1b[34m'), 'blue for URL should be present'); + assert.ok(stdout.includes('\x1b[4m'), 'underline for URL should be present'); + })); +} + +// Test: NO_COLOR=1 produces plain output +{ + const env = { ...process.env, NO_COLOR: '1' }; + delete env.FORCE_COLOR; + execFile(process.execPath, ['--help'], { env }, + common.mustSucceed((stdout) => { + assert.ok(stripAnsi(stdout) === stdout, + 'no ANSI escape sequences should be present with NO_COLOR=1'); + })); +} + +// Test: piped (non-TTY, no FORCE_COLOR) produces plain output +{ + const env = { ...process.env }; + delete env.FORCE_COLOR; + delete env.NO_COLOR; + delete env.NODE_DISABLE_COLORS; + execFile(process.execPath, ['--help'], { env }, + common.mustSucceed((stdout) => { + assert.ok(stripAnsi(stdout) === stdout, + 'no ANSI escape sequences should be present when piped (non-TTY)'); + })); +} + +// Test: alignment preservation - stripped styled output matches plain output +{ + const envStyled = { ...process.env, FORCE_COLOR: '1' }; + const envPlain = { ...process.env, NO_COLOR: '1' }; + delete envPlain.FORCE_COLOR; + execFile(process.execPath, ['--help'], { env: envStyled }, + common.mustSucceed((styledStdout) => { + execFile(process.execPath, ['--help'], { env: envPlain }, + common.mustSucceed((plainStdout) => { + assert.ok(stripAnsi(styledStdout) === plainStdout, + 'stripped styled output should match plain output (alignment preservation)'); + })); + })); +}