util: add util.inspect compact option

The current default formatting is not ideal and this improves
the situation by formatting the output more intuitiv.

1) All object keys are now indented by 2 characters instead of
   sometimes 2 and sometimes 3 characters.
2) Each object key will now use an individual line instead of
   sharing a line potentially with multiple object keys.
3) Long strings will now be split into multiple lines in case
   they exceed the "lineBreak" option length (including the
   current indentation).
4) Opening braces are now directly behind a object property
   instead of using a new line.
5) Switch inspect "base" order. In case the compact option is set
   to `false`, inspect will now print
     "[Function: foo] {\n  property: 'data'\n}"
   instead of
     "{ [Function: foo]\n  property: 'data'\n}".

PR-URL: https://github.com/nodejs/node/pull/17576
Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
Ruben Bridgewater 2017-12-09 05:27:21 -02:00
parent 2d374916eb
commit c2203cb4dd
No known key found for this signature in database
GPG Key ID: F07496B3EB3C1762
3 changed files with 271 additions and 17 deletions

View File

@ -322,6 +322,9 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/REPLACEME
description: The `compact` option is supported now.
- version: v6.6.0
pr-url: https://github.com/nodejs/node/pull/8174
description: Custom inspection functions can now return `this`.
@ -360,6 +363,13 @@ changes:
* `breakLength` {number} The length at which an object's keys are split
across multiple lines. Set to `Infinity` to format an object as a single
line. Defaults to 60 for legacy compatibility.
* `compact` {boolean} Setting this to `false` changes the default indentation
to use a line break for each object key instead of lining up multiple
properties in one line. It will also break text that is above the
`breakLength` size into smaller and better readable chunks and indents
objects the same as arrays. Note that no text will be reduced below 16
characters, no matter the `breakLength` size. For more information, see the
example below. Defaults to `true`.
The `util.inspect()` method returns a string representation of `object` that is
intended for debugging. The output of `util.inspect` may change at any time
@ -396,6 +406,63 @@ Values may supply their own custom `inspect(depth, opts)` functions, when
called these receive the current `depth` in the recursive inspection, as well as
the options object passed to `util.inspect()`.
The following example highlights the difference with the `compact` option:
```js
const util = require('util');
const o = {
a: [1, 2, [[
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do ' +
'eiusmod tempor incididunt ut labore et dolore magna aliqua.',
'test',
'foo']], 4],
b: new Map([['za', 1], ['zb', 'test']])
};
console.log(util.inspect(o, { compact: true, depth: 5, breakLength: 80 }));
// This will print
// { a:
// [ 1,
// 2,
// [ [ 'Lorem ipsum dolor sit amet, consectetur [...]', // A long line
// 'test',
// 'foo' ] ],
// 4 ],
// b: Map { 'za' => 1, 'zb' => 'test' } }
// Setting `compact` to false changes the output to be more reader friendly.
console.log(util.inspect(o, { compact: false, depth: 5, breakLength: 80 }));
// {
// a: [
// 1,
// 2,
// [
// [
// 'Lorem ipsum dolor sit amet, consectetur ' +
// 'adipiscing elit, sed do eiusmod tempor ' +
// 'incididunt ut labore et dolore magna ' +
// 'aliqua.,
// 'test',
// 'foo'
// ]
// ],
// 4
// ],
// b: Map {
// 'za' => 1,
// 'zb' => 'test'
// }
// }
// Setting `breakLength` to e.g. 150 will print the "Lorem ipsum" text in a
// single line.
// Reducing the `breakLength` will split the "Lorem ipsum" text in smaller
// chunks.
```
### Customizing `util.inspect` colors
<!-- type=misc -->

View File

@ -69,7 +69,8 @@ const inspectDefaultOptions = Object.seal({
customInspect: true,
showProxy: false,
maxArrayLength: 100,
breakLength: 60
breakLength: 60,
compact: true
});
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
@ -87,6 +88,10 @@ const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/;
const colorRegExp = /\u001b\[\d\d?m/g;
const numberRegExp = /^(0|[1-9][0-9]*)$/;
const readableRegExps = {};
const MIN_LINE_LENGTH = 16;
// Escaped special characters. Use empty strings to fill up unused entries.
const meta = [
'\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004',
@ -277,7 +282,8 @@ function inspect(obj, opts) {
showProxy: inspectDefaultOptions.showProxy,
maxArrayLength: inspectDefaultOptions.maxArrayLength,
breakLength: inspectDefaultOptions.breakLength,
indentationLvl: 0
indentationLvl: 0,
compact: inspectDefaultOptions.compact
};
// Legacy...
if (arguments.length > 2) {
@ -363,7 +369,7 @@ function stylizeNoColor(str, styleType) {
function formatValue(ctx, value, recurseTimes, ln) {
// Primitive types cannot have properties
if (typeof value !== 'object' && typeof value !== 'function') {
return formatPrimitive(ctx.stylize, value);
return formatPrimitive(ctx.stylize, value, ctx);
}
if (value === null) {
return ctx.stylize('null', 'null');
@ -485,10 +491,10 @@ function formatValue(ctx, value, recurseTimes, ln) {
} catch (e) { /* ignore */ }
if (typeof raw === 'string') {
const formatted = formatPrimitive(stylizeNoColor, raw);
const formatted = formatPrimitive(stylizeNoColor, raw, ctx);
if (keyLength === raw.length)
return ctx.stylize(`[String: ${formatted}]`, 'string');
base = ` [String: ${formatted}]`;
base = `[String: ${formatted}]`;
// For boxed Strings, we have to remove the 0-n indexed entries,
// since they just noisy up the output and are redundant
// Make boxed primitive Strings look like such
@ -510,12 +516,12 @@ function formatValue(ctx, value, recurseTimes, ln) {
`${constructor || tag}${value.name ? `: ${value.name}` : ''}`;
if (keyLength === 0)
return ctx.stylize(`[${name}]`, 'special');
base = ` [${name}]`;
base = `[${name}]`;
} else if (isRegExp(value)) {
// Make RegExps say that they are RegExps
if (keyLength === 0 || recurseTimes < 0)
return ctx.stylize(regExpToString.call(value), 'regexp');
base = ` ${regExpToString.call(value)}`;
base = `${regExpToString.call(value)}`;
} else if (isDate(value)) {
if (keyLength === 0) {
if (Number.isNaN(value.getTime()))
@ -523,12 +529,12 @@ function formatValue(ctx, value, recurseTimes, ln) {
return ctx.stylize(dateToISOString.call(value), 'date');
}
// Make dates with properties first say the date
base = ` ${dateToISOString.call(value)}`;
base = `${dateToISOString.call(value)}`;
} else if (isError(value)) {
// Make error with message first say the error
if (keyLength === 0)
return formatError(value);
base = ` ${formatError(value)}`;
base = `${formatError(value)}`;
} else if (isAnyArrayBuffer(value)) {
// Fast path for ArrayBuffer and SharedArrayBuffer.
// Can't do the same for DataView because it has a non-primitive
@ -558,13 +564,13 @@ function formatValue(ctx, value, recurseTimes, ln) {
const formatted = formatPrimitive(stylizeNoColor, raw);
if (keyLength === 0)
return ctx.stylize(`[Number: ${formatted}]`, 'number');
base = ` [Number: ${formatted}]`;
base = `[Number: ${formatted}]`;
} else if (typeof raw === 'boolean') {
// Make boxed primitive Booleans look like such
const formatted = formatPrimitive(stylizeNoColor, raw);
if (keyLength === 0)
return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean');
base = ` [Boolean: ${formatted}]`;
base = `[Boolean: ${formatted}]`;
} else if (typeof raw === 'symbol') {
const formatted = formatPrimitive(stylizeNoColor, raw);
return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol');
@ -607,9 +613,42 @@ function formatNumber(fn, value) {
return fn(`${value}`, 'number');
}
function formatPrimitive(fn, value) {
if (typeof value === 'string')
function formatPrimitive(fn, value, ctx) {
if (typeof value === 'string') {
if (ctx.compact === false &&
value.length > MIN_LINE_LENGTH &&
ctx.indentationLvl + value.length > ctx.breakLength) {
// eslint-disable-next-line max-len
const minLineLength = Math.max(ctx.breakLength - ctx.indentationLvl, MIN_LINE_LENGTH);
// eslint-disable-next-line max-len
const averageLineLength = Math.ceil(value.length / Math.ceil(value.length / minLineLength));
const divisor = Math.max(averageLineLength, MIN_LINE_LENGTH);
var res = '';
if (readableRegExps[divisor] === undefined) {
// Build a new RegExp that naturally breaks text into multiple lines.
//
// Rules
// 1. Greedy match all text up the max line length that ends with a
// whitespace or the end of the string.
// 2. If none matches, non-greedy match any text up to a whitespace or
// the end of the string.
//
// eslint-disable-next-line max-len, no-unescaped-regexp-dot
readableRegExps[divisor] = new RegExp(`(.|\\n){1,${divisor}}(\\s|$)|(\\n|.)+?(\\s|$)`, 'gm');
}
const indent = ' '.repeat(ctx.indentationLvl);
const matches = value.match(readableRegExps[divisor]);
if (matches.length > 1) {
res += `${fn(strEscape(matches[0]), 'string')} +\n`;
for (var i = 1; i < matches.length - 1; i++) {
res += `${indent} ${fn(strEscape(matches[i]), 'string')} +\n`;
}
res += `${indent} ${fn(strEscape(matches[i]), 'string')}`;
return res;
}
}
return fn(strEscape(value), 'string');
}
if (typeof value === 'number')
return formatNumber(fn, value);
if (typeof value === 'boolean')
@ -820,7 +859,7 @@ function formatProperty(ctx, value, recurseTimes, key, array) {
const desc = Object.getOwnPropertyDescriptor(value, key) ||
{ value: value[key], enumerable: true };
if (desc.value !== undefined) {
const diff = array === 0 ? 3 : 2;
const diff = array !== 0 || ctx.compact === false ? 2 : 3;
ctx.indentationLvl += diff;
str = formatValue(ctx, desc.value, recurseTimes, array === 0);
ctx.indentationLvl -= diff;
@ -853,9 +892,19 @@ function formatProperty(ctx, value, recurseTimes, key, array) {
function reduceToSingleString(ctx, output, base, braces, addLn) {
const breakLength = ctx.breakLength;
var i = 0;
if (ctx.compact === false) {
const indentation = ' '.repeat(ctx.indentationLvl);
var res = `${base ? `${base} ` : ''}${braces[0]}\n${indentation} `;
for (; i < output.length - 1; i++) {
res += `${output[i]},\n${indentation} `;
}
res += `${output[i]}\n${indentation}${braces[1]}`;
return res;
}
if (output.length * 2 <= breakLength) {
var length = 0;
for (var i = 0; i < output.length && length <= breakLength; i++) {
for (; i < output.length && length <= breakLength; i++) {
if (ctx.colors) {
length += output[i].replace(colorRegExp, '').length + 1;
} else {
@ -863,7 +912,8 @@ function reduceToSingleString(ctx, output, base, braces, addLn) {
}
}
if (length <= breakLength)
return `${braces[0]}${base} ${join(output, ', ')} ${braces[1]}`;
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
braces[1];
}
// If the opening "brace" is too large, like in the case of "Set {",
// we need to force the first item to be on the next line or the
@ -871,7 +921,7 @@ function reduceToSingleString(ctx, output, base, braces, addLn) {
const indentation = ' '.repeat(ctx.indentationLvl);
const extraLn = addLn === true ? `\n${indentation}` : '';
const ln = base === '' && braces[0].length === 1 ?
' ' : `${base}\n${indentation} `;
' ' : `${base ? ` ${base}` : base}\n${indentation} `;
const str = join(output, `,\n${indentation} `);
return `${extraLn}${braces[0]}${ln}${str} ${braces[1]}`;
}

View File

@ -1213,3 +1213,140 @@ assert.doesNotThrow(() => util.inspect(process));
assert.strictEqual(util.inspect(new NotStringClass()),
'NotStringClass {}');
}
{
const o = {
a: [1, 2, [[
'Lorem ipsum dolor\nsit amet,\tconsectetur adipiscing elit, sed do ' +
'eiusmod tempor incididunt ut labore et dolore magna aliqua.',
'test',
'foo']], 4],
b: new Map([['za', 1], ['zb', 'test']])
};
let out = util.inspect(o, { compact: true, depth: 5, breakLength: 80 });
let expect = [
'{ a: ',
' [ 1,',
' 2,',
" [ [ 'Lorem ipsum dolor\\nsit amet,\\tconsectetur adipiscing elit, " +
"sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',",
" 'test',",
" 'foo' ] ],",
' 4 ],',
" b: Map { 'za' => 1, 'zb' => 'test' } }",
].join('\n');
assert.strictEqual(out, expect);
out = util.inspect(o, { compact: false, depth: 5, breakLength: 60 });
expect = [
'{',
' a: [',
' 1,',
' 2,',
' [',
' [',
' \'Lorem ipsum dolor\\nsit amet,\\tconsectetur \' +',
' \'adipiscing elit, sed do eiusmod tempor \' +',
' \'incididunt ut labore et dolore magna \' +',
' \'aliqua.\',',
' \'test\',',
' \'foo\'',
' ]',
' ],',
' 4',
' ],',
' b: Map {',
' \'za\' => 1,',
' \'zb\' => \'test\'',
' }',
'}'
].join('\n');
assert.strictEqual(out, expect);
out = util.inspect(o.a[2][0][0], { compact: false, breakLength: 30 });
expect = [
'\'Lorem ipsum dolor\\nsit \' +',
' \'amet,\\tconsectetur \' +',
' \'adipiscing elit, sed do \' +',
' \'eiusmod tempor incididunt \' +',
' \'ut labore et dolore magna \' +',
' \'aliqua.\''
].join('\n');
assert.strictEqual(out, expect);
out = util.inspect(
'12345678901234567890123456789012345678901234567890',
{ compact: false, breakLength: 3 });
expect = '\'12345678901234567890123456789012345678901234567890\'';
assert.strictEqual(out, expect);
out = util.inspect(
'12 45 78 01 34 67 90 23 56 89 123456789012345678901234567890',
{ compact: false, breakLength: 3 });
expect = [
'\'12 45 78 01 34 \' +',
' \'67 90 23 56 89 \' +',
' \'123456789012345678901234567890\''
].join('\n');
assert.strictEqual(out, expect);
out = util.inspect(
'12 45 78 01 34 67 90 23 56 89 1234567890123 0',
{ compact: false, breakLength: 3 });
expect = [
'\'12 45 78 01 34 \' +',
' \'67 90 23 56 89 \' +',
' \'1234567890123 0\''
].join('\n');
assert.strictEqual(out, expect);
out = util.inspect(
'12 45 78 01 34 67 90 23 56 89 12345678901234567 0',
{ compact: false, breakLength: 3 });
expect = [
'\'12 45 78 01 34 \' +',
' \'67 90 23 56 89 \' +',
' \'12345678901234567 \' +',
' \'0\''
].join('\n');
assert.strictEqual(out, expect);
o.a = () => {};
o.b = new Number(3);
out = util.inspect(o, { compact: false, breakLength: 3 });
expect = [
'{',
' a: [Function],',
' b: [Number: 3]',
'}'
].join('\n');
assert.strictEqual(out, expect);
out = util.inspect(o, { compact: false, breakLength: 3, showHidden: true });
expect = [
'{',
' a: [Function] {',
' [length]: 0,',
" [name]: ''",
' },',
' b: [Number: 3]',
'}'
].join('\n');
assert.strictEqual(out, expect);
o[util.inspect.custom] = () => 42;
out = util.inspect(o, { compact: false, breakLength: 3 });
expect = '42';
assert.strictEqual(out, expect);
o[util.inspect.custom] = () => '12 45 78 01 34 67 90 23';
out = util.inspect(o, { compact: false, breakLength: 3 });
expect = '12 45 78 01 34 67 90 23';
assert.strictEqual(out, expect);
o[util.inspect.custom] = () => ({ a: '12 45 78 01 34 67 90 23' });
out = util.inspect(o, { compact: false, breakLength: 3 });
expect = '{\n a: \'12 45 78 01 34 \' +\n \'67 90 23\'\n}';
assert.strictEqual(out, expect);
}