Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

console: add color support #19372

Closed
wants to merge 5 commits into from
Closed
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: 27 additions & 6 deletions doc/api/console.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ changes:
- version: v8.0.0
pr-url: https://github.com/nodejs/node/pull/9744
description: Errors that occur while writing to the underlying streams
will now be ignored.
will now be ignored by default.
-->


<!--type=class-->

The `Console` class can be used to create a simple logger with configurable
Expand All @@ -77,9 +78,29 @@ const { Console } = require('console');
const { Console } = console;
```

### new Console(stdout[, stderr])
* `stdout` {stream.Writable}
* `stderr` {stream.Writable}
### new Console(stdout[, stderr][, ignoreErrors])
### new Console(options)
<!-- YAML
changes:
- version: v8.0.0
pr-url: https://github.com/nodejs/node/pull/9744
description: The `ignoreErrors` option was introduced.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/19372
description: The `Console` constructor now supports an `options` argument,
and the `colorMode` option was introduced.
-->

* `options` {Object}
* `stdout` {stream.Writable}
* `stderr` {stream.Writable}
* `ignoreErrors` {boolean} Ignore errors when writing to the underlying
streams. **Default:** `true`.
* `colorMode` {boolean|string} Set color support for this `Console` instance.
Setting to `true` enables coloring while inspecting values, setting to
`'auto'` will make color support depend on the value of the `isTTY` property
and the value returned by `getColorDepth()` on the respective stream.
**Default:** `'auto'`

Creates a new `Console` with one or two writable stream instances. `stdout` is a
writable stream to print log or info output. `stderr` is used for warning or
Expand All @@ -89,7 +110,7 @@ error output. If `stderr` is not provided, `stdout` is used for `stderr`.
const output = fs.createWriteStream('./stdout.log');
const errorOutput = fs.createWriteStream('./stderr.log');
// custom simple logger
const logger = new Console(output, errorOutput);
const logger = new Console({ stdout: output, stderr: errorOutput });
// use it like console
const count = 5;
logger.log('count: %d', count);
Expand All @@ -100,7 +121,7 @@ The global `console` is a special `Console` whose output is sent to
[`process.stdout`][] and [`process.stderr`][]. It is equivalent to calling:

```js
new Console(process.stdout, process.stderr);
new Console({ stdout: process.stdout, stderr: process.stderr });
```

### console.assert(value[, ...message])
Expand Down
19 changes: 19 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,24 @@ intended as a debugging tool. Some input values can have a significant
performance overhead that can block the event loop. Use this function
with care and never in a hot code path.

## util.formatWithOptions(inspectOptions, format[, ...args])
<!-- YAML
added: REPLACEME
-->

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is worth to just mention the parameter types:

* `inspectOptions` {Object}
* `format` {string}

* `inspectOptions` {Object}
* `format` {string}

This function is identical to [`util.format()`][], except in that it takes
an `inspectOptions` argument which specifies options that are passed along to
[`util.inspect()`][].

```js
util.formatWithOptions({ colors: true }, 'See object %O', { foo: 42 });
// Returns 'See object { foo: 42 }', where `42` is colored as a number
// when printed to a terminal.
```

## util.getSystemErrorName(err)
<!-- YAML
added: v9.7.0
Expand Down Expand Up @@ -2054,6 +2072,7 @@ Deprecated predecessor of `console.log`.
[`Set`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer
[`TypedArray`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
[`util.format()`]: #util_util_format_format_args
[`util.inspect()`]: #util_util_inspect_object_options
[`util.promisify()`]: #util_util_promisify_original
[`util.types.isAnyArrayBuffer()`]: #util_util_types_isanyarraybuffer_value
Expand Down
80 changes: 65 additions & 15 deletions lib/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const {
codes: {
ERR_CONSOLE_WRITABLE_STREAM,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');
const { previewMapIterator, previewSetIterator } = require('internal/v8');
Expand All @@ -49,18 +50,38 @@ const {
} = Array;

// Track amount of indentation required via `console.group()`.
const kGroupIndent = Symbol('groupIndent');
const kGroupIndent = Symbol('kGroupIndent');

function Console(stdout, stderr, ignoreErrors = true) {
const kFormatForStderr = Symbol('kFormatForStderr');
const kFormatForStdout = Symbol('kFormatForStdout');
const kGetInspectOptions = Symbol('kGetInspectOptions');
const kColorMode = Symbol('kColorMode');

function Console(options /* or: stdout, stderr, ignoreErrors = true */) {
if (!(this instanceof Console)) {
return new Console(stdout, stderr, ignoreErrors);
return new Console(...arguments);
}

let stdout, stderr, ignoreErrors, colorMode;
if (options && typeof options.write !== 'function') {
({
stdout,
stderr = stdout,
ignoreErrors = true,
colorMode = 'auto'
} = options);
} else {
return new Console({
stdout: options,
stderr: arguments[1],
ignoreErrors: arguments[2]
});
}

if (!stdout || typeof stdout.write !== 'function') {
throw new ERR_CONSOLE_WRITABLE_STREAM('stdout');
}
if (!stderr) {
stderr = stdout;
} else if (typeof stderr.write !== 'function') {
if (!stderr || typeof stderr.write !== 'function') {
throw new ERR_CONSOLE_WRITABLE_STREAM('stderr');
}

Expand All @@ -82,7 +103,11 @@ function Console(stdout, stderr, ignoreErrors = true) {
prop.value = createWriteErrorHandler(stderr);
Object.defineProperty(this, '_stderrErrorHandler', prop);

if (typeof colorMode !== 'boolean' && colorMode !== 'auto')
throw new ERR_INVALID_ARG_VALUE('colorMode', colorMode);

this[kCounts] = new Map();
this[kColorMode] = colorMode;

Object.defineProperty(this, kGroupIndent, { writable: true });
this[kGroupIndent] = '';
Expand Down Expand Up @@ -144,13 +169,33 @@ function write(ignoreErrors, stream, string, errorhandler, groupIndent) {
}
}

const kColorInspectOptions = { colors: true };
const kNoColorInspectOptions = {};
Console.prototype[kGetInspectOptions] = function(stream) {
let color = this[kColorMode];
if (color === 'auto') {
color = stream.isTTY && (
typeof stream.getColorDepth === 'function' ?
stream.getColorDepth() > 2 : true);
}

return color ? kColorInspectOptions : kNoColorInspectOptions;
};

Console.prototype[kFormatForStdout] = function(args) {
const opts = this[kGetInspectOptions](this._stdout);
return util.formatWithOptions(opts, ...args);
};

Console.prototype[kFormatForStderr] = function(args) {
const opts = this[kGetInspectOptions](this._stderr);
return util.formatWithOptions(opts, ...args);
};

Console.prototype.log = function log(...args) {
write(this._ignoreErrors,
this._stdout,
// The performance of .apply and the spread operator seems on par in V8
// 6.3 but the spread operator, unlike .apply(), pushes the elements
// onto the stack. That is, it makes stack overflows more likely.
util.format.apply(null, args),
this[kFormatForStdout](args),
this._stdoutErrorHandler,
this[kGroupIndent]);
};
Expand All @@ -161,14 +206,16 @@ Console.prototype.dirxml = Console.prototype.log;
Console.prototype.warn = function warn(...args) {
write(this._ignoreErrors,
this._stderr,
util.format.apply(null, args),
this[kFormatForStderr](args),
this._stderrErrorHandler,
this[kGroupIndent]);
};
Console.prototype.error = Console.prototype.warn;

Console.prototype.dir = function dir(object, options) {
options = Object.assign({ customInspect: false }, options);
options = Object.assign({
customInspect: false
}, this[kGetInspectOptions](this._stdout), options);
write(this._ignoreErrors,
this._stdout,
util.inspect(object, options),
Expand Down Expand Up @@ -199,7 +246,7 @@ Console.prototype.timeEnd = function timeEnd(label = 'default') {
Console.prototype.trace = function trace(...args) {
const err = {
name: 'Trace',
message: util.format.apply(null, args)
message: this[kFormatForStderr](args)
};
Error.captureStackTrace(err, trace);
this.error(err.stack);
Expand All @@ -208,7 +255,7 @@ Console.prototype.trace = function trace(...args) {
Console.prototype.assert = function assert(expression, ...args) {
if (!expression) {
args[0] = `Assertion failed${args.length === 0 ? '' : `: ${args[0]}`}`;
this.warn(util.format.apply(null, args));
this.warn(this[kFormatForStderr](args));
}
};

Expand Down Expand Up @@ -369,7 +416,10 @@ Console.prototype.table = function(tabularData, properties) {
return final(keys, values);
};

module.exports = new Console(process.stdout, process.stderr);
module.exports = new Console({
stdout: process.stdout,
stderr: process.stderr
});
module.exports.Console = Console;

function noop() {}
33 changes: 22 additions & 11 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,23 +173,28 @@ function tryStringify(arg) {
}
}

function format(f) {
const emptyOptions = {};
function format(...args) {
return formatWithOptions(emptyOptions, ...args);
}

function formatWithOptions(inspectOptions, f) {
let i, tempStr;
if (typeof f !== 'string') {
if (arguments.length === 0) return '';
if (arguments.length === 1) return '';
let res = '';
for (i = 0; i < arguments.length - 1; i++) {
res += inspect(arguments[i]);
for (i = 1; i < arguments.length - 1; i++) {
res += inspect(arguments[i], inspectOptions);
res += ' ';
}
res += inspect(arguments[i]);
res += inspect(arguments[i], inspectOptions);
return res;
}

if (arguments.length === 1) return f;
if (arguments.length === 2) return f;

let str = '';
let a = 1;
let a = 2;
let lastPos = 0;
for (i = 0; i < f.length - 1; i++) {
if (f.charCodeAt(i) === 37) { // '%'
Expand All @@ -206,12 +211,17 @@ function format(f) {
tempStr = `${Number(arguments[a++])}`;
break;
case 79: // 'O'
tempStr = inspect(arguments[a++]);
tempStr = inspect(arguments[a++], inspectOptions);
break;
case 111: // 'o'
tempStr = inspect(arguments[a++],
{ showHidden: true, showProxy: true });
{
const opts = Object.assign({}, inspectOptions, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: how about

const opts = Object.assign({
  showHidden: true,
  showProxy: true
}, inspectOptions);

Feel free to ignore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lpinca This is kind of intentional … we document that the %o specifier works this way, so I wouldn’t expect per-call options to override it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what case would it be overridden? It's still a new copy per call no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lpinca I meant, if inspectOptions happens to contains showProxy: false, then that would override the behaviour of %o with your suggestion, whereas right now the behaviour of %o is left untouched.

If you do think that that is the right thing to do, then I’m okay with that; I’d prefer to keep this as it is currently documented, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, ignore my comment.

showHidden: true,
showProxy: true
});
tempStr = inspect(arguments[a++], opts);
break;
}
case 105: // 'i'
tempStr = `${parseInt(arguments[a++])}`;
break;
Expand Down Expand Up @@ -244,7 +254,7 @@ function format(f) {
if ((typeof x !== 'object' && typeof x !== 'symbol') || x === null) {
str += ` ${x}`;
} else {
str += ` ${inspect(x)}`;
str += ` ${inspect(x, inspectOptions)}`;
}
}
return str;
Expand Down Expand Up @@ -1206,6 +1216,7 @@ module.exports = exports = {
debuglog,
deprecate,
format,
formatWithOptions,
getSystemErrorName,
inherits,
inspect,
Expand Down
46 changes: 46 additions & 0 deletions test/parallel/test-console-tty-colors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const util = require('util');
const { Writable } = require('stream');
const { Console } = require('console');

function check(isTTY, colorMode, expectedColorMode) {
const items = [
1,
{ a: 2 },
[ 'foo' ],
{ '\\a': '\\bar' }
];

let i = 0;
const stream = new Writable({
write: common.mustCall((chunk, enc, cb) => {
assert.strictEqual(chunk.trim(),
util.inspect(items[i++], {
colors: expectedColorMode
}));
cb();
}, items.length),
decodeStrings: false
});
stream.isTTY = isTTY;

// Set ignoreErrors to `false` here so that we see assertion failures
// from the `write()` call happen.
const testConsole = new Console({
stdout: stream,
ignoreErrors: false,
colorMode
});
for (const item of items) {
testConsole.log(item);
}
}

check(true, 'auto', true);
check(false, 'auto', false);
check(true, true, true);
check(false, true, true);
check(true, false, false);
check(false, false, false);
2 changes: 2 additions & 0 deletions test/parallel/test-console.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ const custom_inspect = { foo: 'bar', inspect: () => 'inspect' };

const strings = [];
const errStrings = [];
process.stdout.isTTY = false;
common.hijackStdout(function(data) {
strings.push(data);
});
process.stderr.isTTY = false;
common.hijackStderr(function(data) {
errStrings.push(data);
});
Expand Down
9 changes: 9 additions & 0 deletions test/pseudo-tty/console_colors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';
require('../common');
// Make this test OS-independent by overriding stdio getColorDepth().
process.stdout.getColorDepth = () => 8;
process.stderr.getColorDepth = () => 8;

console.log({ foo: 'bar' });
console.log('%s q', 'string');
console.log('%o with object format param', { foo: 'bar' });
3 changes: 3 additions & 0 deletions test/pseudo-tty/console_colors.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{ foo: *[32m'bar'*[39m }
string q
{ foo: *[32m'bar'*[39m } with object format param