Skip to content

Commit c18ac52

Browse files
BridgeARMylesBorins
authored andcommitted
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}". Backport-PR-URL: #19230 PR-URL: #17576 Reviewed-By: Anna Henningsen <[email protected]>
1 parent ce3a5af commit c18ac52

File tree

3 files changed

+277
-17
lines changed

3 files changed

+277
-17
lines changed

doc/api/util.md

+67
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,9 @@ stream.write('With ES6');
316316
<!-- YAML
317317
added: v0.3.0
318318
changes:
319+
- version: REPLACEME
320+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
321+
description: The `compact` option is supported now.
319322
- version: v6.6.0
320323
pr-url: https://github.com/nodejs/node/pull/8174
321324
description: Custom inspection functions can now return `this`.
@@ -353,6 +356,13 @@ changes:
353356
* `breakLength` {number} The length at which an object's keys are split
354357
across multiple lines. Set to `Infinity` to format an object as a single
355358
line. Defaults to 60 for legacy compatibility.
359+
* `compact` {boolean} Setting this to `false` changes the default indentation
360+
to use a line break for each object key instead of lining up multiple
361+
properties in one line. It will also break text that is above the
362+
`breakLength` size into smaller and better readable chunks and indents
363+
objects the same as arrays. Note that no text will be reduced below 16
364+
characters, no matter the `breakLength` size. For more information, see the
365+
example below. Defaults to `true`.
356366

357367
The `util.inspect()` method returns a string representation of `object` that is
358368
primarily useful for debugging. Additional `options` may be passed that alter
@@ -370,6 +380,63 @@ Values may supply their own custom `inspect(depth, opts)` functions, when
370380
called these receive the current `depth` in the recursive inspection, as well as
371381
the options object passed to `util.inspect()`.
372382

383+
The following example highlights the difference with the `compact` option:
384+
385+
```js
386+
const util = require('util');
387+
388+
const o = {
389+
a: [1, 2, [[
390+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do ' +
391+
'eiusmod tempor incididunt ut labore et dolore magna aliqua.',
392+
'test',
393+
'foo']], 4],
394+
b: new Map([['za', 1], ['zb', 'test']])
395+
};
396+
console.log(util.inspect(o, { compact: true, depth: 5, breakLength: 80 }));
397+
398+
// This will print
399+
400+
// { a:
401+
// [ 1,
402+
// 2,
403+
// [ [ 'Lorem ipsum dolor sit amet, consectetur [...]', // A long line
404+
// 'test',
405+
// 'foo' ] ],
406+
// 4 ],
407+
// b: Map { 'za' => 1, 'zb' => 'test' } }
408+
409+
// Setting `compact` to false changes the output to be more reader friendly.
410+
console.log(util.inspect(o, { compact: false, depth: 5, breakLength: 80 }));
411+
412+
// {
413+
// a: [
414+
// 1,
415+
// 2,
416+
// [
417+
// [
418+
// 'Lorem ipsum dolor sit amet, consectetur ' +
419+
// 'adipiscing elit, sed do eiusmod tempor ' +
420+
// 'incididunt ut labore et dolore magna ' +
421+
// 'aliqua.,
422+
// 'test',
423+
// 'foo'
424+
// ]
425+
// ],
426+
// 4
427+
// ],
428+
// b: Map {
429+
// 'za' => 1,
430+
// 'zb' => 'test'
431+
// }
432+
// }
433+
434+
// Setting `breakLength` to e.g. 150 will print the "Lorem ipsum" text in a
435+
// single line.
436+
// Reducing the `breakLength` will split the "Lorem ipsum" text in smaller
437+
// chunks.
438+
```
439+
373440
### Customizing `util.inspect` colors
374441

375442
<!-- type=misc -->

lib/util.js

+67-17
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ const inspectDefaultOptions = Object.seal({
6868
customInspect: true,
6969
showProxy: false,
7070
maxArrayLength: 100,
71-
breakLength: 60
71+
breakLength: 60,
72+
compact: true
7273
});
7374

7475
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable;
@@ -86,6 +87,10 @@ const strEscapeSequencesReplacer = /[\x00-\x1f\x27\x5c]/g;
8687
const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/;
8788
const numberRegExp = /^(0|[1-9][0-9]*)$/;
8889

90+
const readableRegExps = {};
91+
92+
const MIN_LINE_LENGTH = 16;
93+
8994
// Escaped special characters. Use empty strings to fill up unused entries.
9095
const meta = [
9196
'\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004',
@@ -276,7 +281,8 @@ function inspect(value, opts) {
276281
showProxy: inspectDefaultOptions.showProxy,
277282
maxArrayLength: inspectDefaultOptions.maxArrayLength,
278283
breakLength: inspectDefaultOptions.breakLength,
279-
indentationLvl: 0
284+
indentationLvl: 0,
285+
compact: inspectDefaultOptions.compact
280286
};
281287
// Legacy...
282288
if (arguments.length > 2) {
@@ -374,7 +380,7 @@ function ensureDebugIsInitialized() {
374380
function formatValue(ctx, value, recurseTimes, ln) {
375381
// Primitive types cannot have properties
376382
if (typeof value !== 'object' && typeof value !== 'function') {
377-
return formatPrimitive(ctx.stylize, value);
383+
return formatPrimitive(ctx.stylize, value, ctx);
378384
}
379385
if (value === null) {
380386
return ctx.stylize('null', 'null');
@@ -481,10 +487,10 @@ function formatValue(ctx, value, recurseTimes, ln) {
481487
} catch (e) { /* ignore */ }
482488

483489
if (typeof raw === 'string') {
484-
const formatted = formatPrimitive(stylizeNoColor, raw);
490+
const formatted = formatPrimitive(stylizeNoColor, raw, ctx);
485491
if (keyLength === raw.length)
486492
return ctx.stylize(`[String: ${formatted}]`, 'string');
487-
base = ` [String: ${formatted}]`;
493+
base = `[String: ${formatted}]`;
488494
// For boxed Strings, we have to remove the 0-n indexed entries,
489495
// since they just noisy up the output and are redundant
490496
// Make boxed primitive Strings look like such
@@ -505,25 +511,25 @@ function formatValue(ctx, value, recurseTimes, ln) {
505511
const name = `${constructor.name}${value.name ? `: ${value.name}` : ''}`;
506512
if (keyLength === 0)
507513
return ctx.stylize(`[${name}]`, 'special');
508-
base = ` [${name}]`;
514+
base = `[${name}]`;
509515
} else if (isRegExp(value)) {
510516
// Make RegExps say that they are RegExps
511517
if (keyLength === 0 || recurseTimes < 0)
512518
return ctx.stylize(regExpToString.call(value), 'regexp');
513-
base = ` ${regExpToString.call(value)}`;
519+
base = `${regExpToString.call(value)}`;
514520
} else if (isDate(value)) {
515521
if (keyLength === 0) {
516522
if (Number.isNaN(value.getTime()))
517523
return ctx.stylize(value.toString(), 'date');
518524
return ctx.stylize(dateToISOString.call(value), 'date');
519525
}
520526
// Make dates with properties first say the date
521-
base = ` ${dateToISOString.call(value)}`;
527+
base = `${dateToISOString.call(value)}`;
522528
} else if (isError(value)) {
523529
// Make error with message first say the error
524530
if (keyLength === 0)
525531
return formatError(value);
526-
base = ` ${formatError(value)}`;
532+
base = `${formatError(value)}`;
527533
} else if (isAnyArrayBuffer(value)) {
528534
// Fast path for ArrayBuffer and SharedArrayBuffer.
529535
// Can't do the same for DataView because it has a non-primitive
@@ -553,13 +559,13 @@ function formatValue(ctx, value, recurseTimes, ln) {
553559
const formatted = formatPrimitive(stylizeNoColor, raw);
554560
if (keyLength === 0)
555561
return ctx.stylize(`[Number: ${formatted}]`, 'number');
556-
base = ` [Number: ${formatted}]`;
562+
base = `[Number: ${formatted}]`;
557563
} else if (typeof raw === 'boolean') {
558564
// Make boxed primitive Booleans look like such
559565
const formatted = formatPrimitive(stylizeNoColor, raw);
560566
if (keyLength === 0)
561567
return ctx.stylize(`[Boolean: ${formatted}]`, 'boolean');
562-
base = ` [Boolean: ${formatted}]`;
568+
base = `[Boolean: ${formatted}]`;
563569
} else if (typeof raw === 'symbol') {
564570
const formatted = formatPrimitive(stylizeNoColor, raw);
565571
return ctx.stylize(`[Symbol: ${formatted}]`, 'symbol');
@@ -603,9 +609,42 @@ function formatNumber(fn, value) {
603609
return fn(`${value}`, 'number');
604610
}
605611

606-
function formatPrimitive(fn, value) {
607-
if (typeof value === 'string')
612+
function formatPrimitive(fn, value, ctx) {
613+
if (typeof value === 'string') {
614+
if (ctx.compact === false &&
615+
value.length > MIN_LINE_LENGTH &&
616+
ctx.indentationLvl + value.length > ctx.breakLength) {
617+
// eslint-disable-next-line max-len
618+
const minLineLength = Math.max(ctx.breakLength - ctx.indentationLvl, MIN_LINE_LENGTH);
619+
// eslint-disable-next-line max-len
620+
const averageLineLength = Math.ceil(value.length / Math.ceil(value.length / minLineLength));
621+
const divisor = Math.max(averageLineLength, MIN_LINE_LENGTH);
622+
var res = '';
623+
if (readableRegExps[divisor] === undefined) {
624+
// Build a new RegExp that naturally breaks text into multiple lines.
625+
//
626+
// Rules
627+
// 1. Greedy match all text up the max line length that ends with a
628+
// whitespace or the end of the string.
629+
// 2. If none matches, non-greedy match any text up to a whitespace or
630+
// the end of the string.
631+
//
632+
// eslint-disable-next-line max-len, no-unescaped-regexp-dot
633+
readableRegExps[divisor] = new RegExp(`(.|\\n){1,${divisor}}(\\s|$)|(\\n|.)+?(\\s|$)`, 'gm');
634+
}
635+
const indent = ' '.repeat(ctx.indentationLvl);
636+
const matches = value.match(readableRegExps[divisor]);
637+
if (matches.length > 1) {
638+
res += `${fn(strEscape(matches[0]), 'string')} +\n`;
639+
for (var i = 1; i < matches.length - 1; i++) {
640+
res += `${indent} ${fn(strEscape(matches[i]), 'string')} +\n`;
641+
}
642+
res += `${indent} ${fn(strEscape(matches[i]), 'string')}`;
643+
return res;
644+
}
645+
}
608646
return fn(strEscape(value), 'string');
647+
}
609648
if (typeof value === 'number')
610649
return formatNumber(fn, value);
611650
if (typeof value === 'boolean')
@@ -806,7 +845,7 @@ function formatProperty(ctx, value, recurseTimes, key, array) {
806845
const desc = Object.getOwnPropertyDescriptor(value, key) ||
807846
{ value: value[key], enumerable: true };
808847
if (desc.value !== undefined) {
809-
const diff = array === 0 ? 3 : 2;
848+
const diff = array !== 0 || ctx.compact === false ? 2 : 3;
810849
ctx.indentationLvl += diff;
811850
str = formatValue(ctx, desc.value, recurseTimes, array === 0);
812851
ctx.indentationLvl -= diff;
@@ -839,25 +878,36 @@ function formatProperty(ctx, value, recurseTimes, key, array) {
839878

840879
function reduceToSingleString(ctx, output, base, braces, addLn) {
841880
const breakLength = ctx.breakLength;
881+
var i = 0;
882+
if (ctx.compact === false) {
883+
const indentation = ' '.repeat(ctx.indentationLvl);
884+
var res = `${base ? `${base} ` : ''}${braces[0]}\n${indentation} `;
885+
for (; i < output.length - 1; i++) {
886+
res += `${output[i]},\n${indentation} `;
887+
}
888+
res += `${output[i]}\n${indentation}${braces[1]}`;
889+
return res;
890+
}
842891
if (output.length * 2 <= breakLength) {
843892
var length = 0;
844-
for (var i = 0; i < output.length && length <= breakLength; i++) {
893+
for (; i < output.length && length <= breakLength; i++) {
845894
if (ctx.colors) {
846895
length += removeColors(output[i]).length + 1;
847896
} else {
848897
length += output[i].length + 1;
849898
}
850899
}
851900
if (length <= breakLength)
852-
return `${braces[0]}${base} ${join(output, ', ')} ${braces[1]}`;
901+
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
902+
braces[1];
853903
}
854904
// If the opening "brace" is too large, like in the case of "Set {",
855905
// we need to force the first item to be on the next line or the
856906
// items will not line up correctly.
857907
const indentation = ' '.repeat(ctx.indentationLvl);
858908
const extraLn = addLn === true ? `\n${indentation}` : '';
859909
const ln = base === '' && braces[0].length === 1 ?
860-
' ' : `${base}\n${indentation} `;
910+
' ' : `${base ? ` ${base}` : base}\n${indentation} `;
861911
const str = join(output, `,\n${indentation} `);
862912
return `${extraLn}${braces[0]}${ln}${str} ${braces[1]}`;
863913
}

0 commit comments

Comments
 (0)