Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4a664b5

Browse filesBrowse files
committed
lib: respect terminal capabilities on styleText
This PR changes styleText API to respect terminal capabilities and environment variables such as NO_COLOR, NODE_DISABLE_COLORS, and FORCE_COLOR. PR-URL: #54389 Fixes: #54365 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Claudio Wunder <cwunder@gnome.org> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent 04f83b5 commit 4a664b5
Copy full SHA for 4a664b5

4 files changed

+146-13Lines changed: 146 additions & 13 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎doc/api/util.md‎

Copy file name to clipboardExpand all lines: doc/api/util.md
+37-6Lines changed: 37 additions & 6 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1800,32 +1800,63 @@ console.log(util.stripVTControlCharacters('\u001B[4mvalue\u001B[0m'));
18001800
// Prints "value"
18011801
```
18021802
1803-
## `util.styleText(format, text)`
1803+
## `util.styleText(format, text[, options])`
18041804
18051805
> Stability: 1.1 - Active development
18061806
18071807
<!-- YAML
18081808
added:
18091809
- v21.7.0
18101810
- v20.12.0
1811+
changes:
1812+
- version: REPLACEME
1813+
pr-url: https://github.com/nodejs/node/pull/54389
1814+
description: Respect isTTY and environment variables
1815+
such as NO_COLORS, NODE_DISABLE_COLORS, and FORCE_COLOR.
18111816
-->
18121817
18131818
* `format` {string | Array} A text format or an Array
18141819
of text formats defined in `util.inspect.colors`.
18151820
* `text` {string} The text to to be formatted.
1821+
* `options` {Object}
1822+
* `validateStream` {boolean} When true, `stream` is checked to see if it can handle colors. **Default:** `true`.
1823+
* `stream` {Stream} A stream that will be validated if it can be colored. **Default:** `process.stdout`.
18161824
1817-
This function returns a formatted text considering the `format` passed.
1825+
This function returns a formatted text considering the `format` passed
1826+
for printing in a terminal, it is aware of the terminal's capabilities
1827+
and act according to the configuration set via `NO_COLORS`,
1828+
`NODE_DISABLE_COLORS` and `FORCE_COLOR` environment variables.
18181829
18191830
```mjs
18201831
import { styleText } from 'node:util';
1821-
const errorMessage = styleText('red', 'Error! Error!');
1822-
console.log(errorMessage);
1832+
import { stderr } from 'node:process';
1833+
1834+
const successMessage = styleText('green', 'Success!');
1835+
console.log(successMessage);
1836+
1837+
const errorMessage = styleText(
1838+
'red',
1839+
'Error! Error!',
1840+
// Validate if process.stderr has TTY
1841+
{ stream: stderr },
1842+
);
1843+
console.error(successMessage);
18231844
```
18241845
18251846
```cjs
18261847
const { styleText } = require('node:util');
1827-
const errorMessage = styleText('red', 'Error! Error!');
1828-
console.log(errorMessage);
1848+
const { stderr } = require('node:process');
1849+
1850+
const successMessage = styleText('green', 'Success!');
1851+
console.log(successMessage);
1852+
1853+
const errorMessage = styleText(
1854+
'red',
1855+
'Error! Error!',
1856+
// Validate if process.stderr has TTY
1857+
{ stream: stderr },
1858+
);
1859+
console.error(successMessage);
18291860
```
18301861
18311862
`util.inspect.colors` also provides text formats such as `italic`, and
Collapse file

‎lib/util.js‎

Copy file name to clipboardExpand all lines: lib/util.js
+41-1Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,26 @@ const {
6565
} = require('internal/util/inspect');
6666
const { debuglog } = require('internal/util/debuglog');
6767
const {
68+
validateBoolean,
6869
validateFunction,
6970
validateNumber,
7071
validateString,
7172
validateOneOf,
7273
} = require('internal/validators');
7374
const { isBuffer } = require('buffer').Buffer;
75+
const {
76+
isReadableStream,
77+
isWritableStream,
78+
isNodeStream,
79+
} = require('internal/streams/utils');
7480
const types = require('internal/util/types');
81+
82+
let utilColors;
83+
function lazyUtilColors() {
84+
utilColors ??= require('internal/util/colors');
85+
return utilColors;
86+
}
87+
7588
const binding = internalBinding('util');
7689

7790
const {
@@ -209,10 +222,25 @@ function escapeStyleCode(code) {
209222
/**
210223
* @param {string | string[]} format
211224
* @param {string} text
225+
* @param {object} [options={}]
226+
* @param {boolean} [options.validateStream=true] - Whether to validate the stream.
227+
* @param {Stream} [options.stream=process.stdout] - The stream used for validation.
212228
* @returns {string}
213229
*/
214-
function styleText(format, text) {
230+
function styleText(format, text, { validateStream = true, stream = process.stdout } = {}) {
215231
validateString(text, 'text');
232+
validateBoolean(validateStream, 'options.validateStream');
233+
234+
if (validateStream) {
235+
if (
236+
!isReadableStream(stream) &&
237+
!isWritableStream(stream) &&
238+
!isNodeStream(stream)
239+
) {
240+
throw new ERR_INVALID_ARG_TYPE('stream', ['ReadableStream', 'WritableStream', 'Stream'], stream);
241+
}
242+
}
243+
216244
if (ArrayIsArray(format)) {
217245
let left = '';
218246
let right = '';
@@ -232,6 +260,18 @@ function styleText(format, text) {
232260
if (formatCodes == null) {
233261
validateOneOf(format, 'format', ObjectKeys(inspect.colors));
234262
}
263+
264+
// Check colorize only after validating arg type and value
265+
if (
266+
validateStream &&
267+
(
268+
!stream ||
269+
!lazyUtilColors().shouldColorize(stream)
270+
)
271+
) {
272+
return text;
273+
}
274+
235275
return `${escapeStyleCode(formatCodes[0])}${text}${escapeStyleCode(formatCodes[1])}`;
236276
}
237277

Collapse file

‎test/parallel/test-bootstrap-modules.js‎

Copy file name to clipboardExpand all lines: test/parallel/test-bootstrap-modules.js
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ expected.beforePreExec = new Set([
4646
'NativeModule internal/assert',
4747
'NativeModule internal/util/inspect',
4848
'NativeModule internal/util/debuglog',
49+
'NativeModule internal/streams/utils',
4950
'NativeModule internal/timers',
5051
'NativeModule events',
5152
'Internal Binding buffer',
Collapse file

‎test/parallel/test-util-styletext.js‎

Copy file name to clipboardExpand all lines: test/parallel/test-util-styletext.js
+67-6Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
'use strict';
2-
require('../common');
3-
const assert = require('assert');
4-
const util = require('util');
2+
3+
const common = require('../common');
4+
const assert = require('node:assert');
5+
const util = require('node:util');
6+
const { WriteStream } = require('node:tty');
7+
8+
const styled = '\u001b[31mtest\u001b[39m';
9+
const noChange = 'test';
510

611
[
712
undefined,
@@ -31,13 +36,69 @@ assert.throws(() => {
3136
code: 'ERR_INVALID_ARG_VALUE',
3237
});
3338

34-
assert.strictEqual(util.styleText('red', 'test'), '\u001b[31mtest\u001b[39m');
39+
assert.strictEqual(
40+
util.styleText('red', 'test', { validateStream: false }),
41+
'\u001b[31mtest\u001b[39m',
42+
);
43+
44+
assert.strictEqual(
45+
util.styleText(['bold', 'red'], 'test', { validateStream: false }),
46+
'\u001b[1m\u001b[31mtest\u001b[39m\u001b[22m',
47+
);
3548

36-
assert.strictEqual(util.styleText(['bold', 'red'], 'test'), '\u001b[1m\u001b[31mtest\u001b[39m\u001b[22m');
37-
assert.strictEqual(util.styleText(['bold', 'red'], 'test'), util.styleText('bold', util.styleText('red', 'test')));
49+
assert.strictEqual(
50+
util.styleText(['bold', 'red'], 'test', { validateStream: false }),
51+
util.styleText(
52+
'bold',
53+
util.styleText('red', 'test', { validateStream: false }),
54+
{ validateStream: false },
55+
),
56+
);
3857

3958
assert.throws(() => {
4059
util.styleText(['invalid'], 'text');
4160
}, {
4261
code: 'ERR_INVALID_ARG_VALUE',
4362
});
63+
64+
assert.throws(() => {
65+
util.styleText('red', 'text', { stream: {} });
66+
}, {
67+
code: 'ERR_INVALID_ARG_TYPE',
68+
});
69+
70+
// does not throw
71+
util.styleText('red', 'text', { stream: {}, validateStream: false });
72+
73+
assert.strictEqual(
74+
util.styleText('red', 'test', { validateStream: false }),
75+
styled,
76+
);
77+
78+
const fd = common.getTTYfd();
79+
if (fd !== -1) {
80+
const writeStream = new WriteStream(fd);
81+
82+
const originalEnv = process.env;
83+
[
84+
{ isTTY: true, env: {}, expected: styled },
85+
{ isTTY: false, env: {}, expected: noChange },
86+
{ isTTY: true, env: { NODE_DISABLE_COLORS: '1' }, expected: noChange },
87+
{ isTTY: true, env: { NO_COLOR: '1' }, expected: noChange },
88+
{ isTTY: true, env: { FORCE_COLOR: '1' }, expected: styled },
89+
{ isTTY: true, env: { FORCE_COLOR: '1', NODE_DISABLE_COLORS: '1' }, expected: styled },
90+
{ isTTY: false, env: { FORCE_COLOR: '1', NO_COLOR: '1', NODE_DISABLE_COLORS: '1' }, expected: styled },
91+
{ isTTY: true, env: { FORCE_COLOR: '1', NO_COLOR: '1', NODE_DISABLE_COLORS: '1' }, expected: styled },
92+
].forEach((testCase) => {
93+
writeStream.isTTY = testCase.isTTY;
94+
process.env = {
95+
...process.env,
96+
...testCase.env
97+
};
98+
const output = util.styleText('red', 'test', { stream: writeStream });
99+
assert.strictEqual(output, testCase.expected);
100+
process.env = originalEnv;
101+
});
102+
} else {
103+
common.skip('Could not create TTY fd');
104+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.