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

util: add util.inspect compact option #17576

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 67 additions & 2 deletions doc/api/util.md
Expand Up @@ -308,6 +308,9 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/REPLACEME
description: The `structured` 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`.
Expand All @@ -334,8 +337,7 @@ changes:
codes. Defaults to `false`. Colors are customizable, see
[Customizing `util.inspect` colors][].
* `customInspect` {boolean} If `false`, then custom `inspect(depth, opts)`
functions exported on the `object` being inspected will not be called.
Defaults to `true`.
functions will not be called. Defaults to `true`.
* `showProxy` {boolean} If `true`, then objects and functions that are
`Proxy` objects will be introspected to show their `target` and `handler`
objects. Defaults to `false`.
Expand All @@ -346,6 +348,12 @@ 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.
* `structured` {boolean} This 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.

The `util.inspect()` method returns a string representation of `object` that is
primarily useful for debugging. Additional `options` may be passed that alter
Expand Down Expand Up @@ -381,6 +389,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 `structured` 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, { structured: false, 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 `structured` to true changes the output to be more readable.
console.log(util.inspect(o, { structured: true, 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 -->
Expand Down
93 changes: 71 additions & 22 deletions lib/util.js
Expand Up @@ -69,7 +69,8 @@ const inspectDefaultOptions = Object.seal({
customInspect: true,
showProxy: false,
maxArrayLength: 100,
breakLength: 60
breakLength: 60,
structured: false
});

const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
Expand All @@ -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 structuredRE = {};

const MIN_LINE_LENGTH = 16;

// Escaped special characters. Use empty strings to fill up unused entries.
const meta = [
'\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004',
Expand Down Expand Up @@ -254,14 +259,14 @@ function debuglog(set) {
}

/**
* Echos the value of a value. Tries to print the value out
* Echos the value of any input. Tries to print the value out
* in the best way possible given the different types.
*
* @param {Object} obj The object to print out.
* @param {any} value The value to print out.
* @param {Object} opts Optional options object that alters the output.
*/
/* Legacy: obj, showHidden, depth, colors*/
function inspect(obj, opts) {
/* Legacy: value, showHidden, depth, colors*/
function inspect(value, opts) {
// Default options
const ctx = {
seen: [],
Expand All @@ -273,7 +278,8 @@ function inspect(obj, opts) {
showProxy: inspectDefaultOptions.showProxy,
maxArrayLength: inspectDefaultOptions.maxArrayLength,
breakLength: inspectDefaultOptions.breakLength,
indentationLvl: 0
indentationLvl: 0,
structured: inspectDefaultOptions.structured
};
// Legacy...
if (arguments.length > 2) {
Expand All @@ -295,7 +301,7 @@ function inspect(obj, opts) {
}
if (ctx.colors) ctx.stylize = stylizeWithColor;
if (ctx.maxArrayLength === null) ctx.maxArrayLength = Infinity;
return formatValue(ctx, obj, ctx.depth);
return formatValue(ctx, value, ctx.depth);
}
inspect.custom = customInspectSymbol;

Expand Down Expand Up @@ -359,7 +365,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');
Expand Down Expand Up @@ -481,10 +487,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
Expand All @@ -506,25 +512,25 @@ 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()))
return ctx.stylize(value.toString(), 'date');
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
Expand Down Expand Up @@ -554,13 +560,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');
Expand Down Expand Up @@ -603,9 +609,41 @@ 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.structured &&
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 (structuredRE[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
structuredRE[divisor] = new RegExp(`.{1,${divisor}}(\\s|$)|.+?(\\s|$)`, 'gm');
const indent = ' '.repeat(ctx.indentationLvl);
const matches = value.match(structuredRE[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')
Expand Down Expand Up @@ -816,7 +854,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.structured === true ? 2 : 3;
ctx.indentationLvl += diff;
str = formatValue(ctx, desc.value, recurseTimes, array === 0);
ctx.indentationLvl -= diff;
Expand Down Expand Up @@ -849,25 +887,36 @@ function formatProperty(ctx, value, recurseTimes, key, array) {

function reduceToSingleString(ctx, output, base, braces, addLn) {
const breakLength = ctx.breakLength;
var i = 0;
if (ctx.structured === true) {
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 {
length += output[i].length + 1;
}
}
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
// items will not line up correctly.
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]}`;
}
Expand Down