Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions lib/internal/main/print_help.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -194,6 +199,7 @@ function format(
aliases,
firstColumn: maxFirstColumnUsed + 2,
secondColumn,
nameStyle,
});
}

Expand All @@ -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);
}

Expand Down
64 changes: 64 additions & 0 deletions test/parallel/test-cli-node-print-help-style.js
Original file line number Diff line number Diff line change
@@ -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)');
}));
}));
}
Loading