diff options
-rw-r--r-- | doc/api/util.md | 50 | ||||
-rw-r--r-- | lib/util.js | 181 | ||||
-rw-r--r-- | test/parallel/test-util-format.js | 16 |
3 files changed, 145 insertions, 102 deletions
diff --git a/doc/api/util.md b/doc/api/util.md index c1fc606a51..3def79546b 100644 --- a/doc/api/util.md +++ b/doc/api/util.md @@ -184,6 +184,17 @@ property take precedence over `--trace-deprecation` and added: v0.5.3 changes: - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/23162 + description: The `format` argument is now only taken as such if it actually + contains format specifiers. + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/23162 + description: If the `format` argument is not a format string, the output + string's formatting is no longer dependent on the type of the + first argument. This change removes previously present quotes + from strings that were being output when the first argument + was not a string. + - version: REPLACEME pr-url: https://github.com/nodejs/node/pull/17907 description: The `%o` specifier's `depth` option will now fall back to the default depth. @@ -195,11 +206,9 @@ changes: * `format` {string} A `printf`-like format string. The `util.format()` method returns a formatted string using the first argument -as a `printf`-like format. - -The first argument is a string containing zero or more *placeholder* tokens. -Each placeholder token is replaced with the converted value from the -corresponding argument. Supported placeholders are: +as a `printf`-like format string which can contain zero or more format +specifiers. Each specifier is replaced with the converted value from the +corresponding argument. Supported specifiers are: * `%s` - `String`. * `%d` - `Number` (integer or floating point value) or `BigInt`. @@ -218,37 +227,40 @@ contains circular references. * `%%` - single percent sign (`'%'`). This does not consume an argument. * Returns: {string} The formatted string -If the placeholder does not have a corresponding argument, the placeholder is -not replaced. +If a specifier does not have a corresponding argument, it is not replaced: ```js util.format('%s:%s', 'foo'); // Returns: 'foo:%s' ``` -If there are more arguments passed to the `util.format()` method than the number -of placeholders, the extra arguments are coerced into strings then concatenated -to the returned string, each delimited by a space. Excessive arguments whose -`typeof` is `'object'` or `'symbol'` (except `null`) will be transformed by -`util.inspect()`. +Values that are not part of the format string are formatted using +`util.inspect()` if their type is either `'object'`, `'symbol'`, `'function'` +or `'number'` and using `String()` in all other cases. + +If there are more arguments passed to the `util.format()` method than the +number of specifiers, the extra arguments are concatenated to the returned +string, separated by spaces: ```js -util.format('%s:%s', 'foo', 'bar', 'baz'); // 'foo:bar baz' +util.format('%s:%s', 'foo', 'bar', 'baz'); +// Returns: 'foo:bar baz' ``` -If the first argument is not a string then `util.format()` returns -a string that is the concatenation of all arguments separated by spaces. -Each argument is converted to a string using `util.inspect()`. +If the first argument does not contain a valid format specifier, `util.format()` +returns a string that is the concatenation of all arguments separated by spaces: ```js -util.format(1, 2, 3); // '1 2 3' +util.format(1, 2, 3); +// Returns: '1 2 3' ``` If only one argument is passed to `util.format()`, it is returned as it is -without any formatting. +without any formatting: ```js -util.format('%% %s'); // '%% %s' +util.format('%% %s'); +// Returns: '%% %s' ``` Please note that `util.format()` is a synchronous method that is mainly diff --git a/lib/util.js b/lib/util.js index 2e42beb58b..22c2b260da 100644 --- a/lib/util.js +++ b/lib/util.js @@ -72,98 +72,113 @@ function format(...args) { return formatWithOptions(emptyOptions, ...args); } -function formatWithOptions(inspectOptions, f) { - let i, tempStr; - if (typeof f !== 'string') { - if (arguments.length === 1) return ''; - let res = ''; - for (i = 1; i < arguments.length - 1; i++) { - res += inspect(arguments[i], inspectOptions); - res += ' '; - } - res += inspect(arguments[i], inspectOptions); - return res; +function formatValue(val, inspectOptions) { + const inspectTypes = ['object', 'symbol', 'function', 'number']; + + if (inspectTypes.includes(typeof val)) { + return inspect(val, inspectOptions); + } else { + return String(val); + } +} + +function formatWithOptions(inspectOptions, ...args) { + const first = args[0]; + const parts = []; + + const firstIsString = typeof first === 'string'; + + if (firstIsString && args.length === 1) { + return first; } - if (arguments.length === 2) return f; - - let str = ''; - let a = 2; - let lastPos = 0; - for (i = 0; i < f.length - 1; i++) { - if (f.charCodeAt(i) === 37) { // '%' - const nextChar = f.charCodeAt(++i); - if (a !== arguments.length) { - switch (nextChar) { - case 115: // 's' - tempStr = String(arguments[a++]); - break; - case 106: // 'j' - tempStr = tryStringify(arguments[a++]); - break; - case 100: // 'd' - const tempNum = arguments[a++]; - // eslint-disable-next-line valid-typeof - if (typeof tempNum === 'bigint') { - tempStr = `${tempNum}n`; - } else { - tempStr = `${Number(tempNum)}`; + if (firstIsString && /%[sjdOoif%]/.test(first)) { + let i, tempStr; + let str = ''; + let a = 1; + let lastPos = 0; + + for (i = 0; i < first.length - 1; i++) { + if (first.charCodeAt(i) === 37) { // '%' + const nextChar = first.charCodeAt(++i); + if (a !== args.length) { + switch (nextChar) { + case 115: // 's' + tempStr = String(args[a++]); + break; + case 106: // 'j' + tempStr = tryStringify(args[a++]); + break; + case 100: // 'd' + const tempNum = args[a++]; + // eslint-disable-next-line valid-typeof + if (typeof tempNum === 'bigint') { + tempStr = `${tempNum}n`; + } else { + tempStr = `${Number(tempNum)}`; + } + break; + case 79: // 'O' + tempStr = inspect(args[a++], inspectOptions); + break; + case 111: // 'o' + { + const opts = Object.assign({}, inspectOptions, { + showHidden: true, + showProxy: true, + depth: 4 + }); + tempStr = inspect(args[a++], opts); + break; } - break; - case 79: // 'O' - tempStr = inspect(arguments[a++], inspectOptions); - break; - case 111: // 'o' - { - const opts = Object.assign({}, inspectOptions, { - showHidden: true, - showProxy: true - }); - tempStr = inspect(arguments[a++], opts); - break; + case 105: // 'i' + const tempInteger = args[a++]; + // eslint-disable-next-line valid-typeof + if (typeof tempInteger === 'bigint') { + tempStr = `${tempInteger}n`; + } else { + tempStr = `${parseInt(tempInteger)}`; + } + break; + case 102: // 'f' + tempStr = `${parseFloat(args[a++])}`; + break; + case 37: // '%' + str += first.slice(lastPos, i); + lastPos = i + 1; + continue; + default: // any other character is not a correct placeholder + continue; } - case 105: // 'i' - const tempInteger = arguments[a++]; - // eslint-disable-next-line valid-typeof - if (typeof tempInteger === 'bigint') { - tempStr = `${tempInteger}n`; - } else { - tempStr = `${parseInt(tempInteger)}`; - } - break; - case 102: // 'f' - tempStr = `${parseFloat(arguments[a++])}`; - break; - case 37: // '%' - str += f.slice(lastPos, i); - lastPos = i + 1; - continue; - default: // any other character is not a correct placeholder - continue; + if (lastPos !== i - 1) { + str += first.slice(lastPos, i - 1); + } + str += tempStr; + lastPos = i + 1; + } else if (nextChar === 37) { + str += first.slice(lastPos, i); + lastPos = i + 1; } - if (lastPos !== i - 1) - str += f.slice(lastPos, i - 1); - str += tempStr; - lastPos = i + 1; - } else if (nextChar === 37) { - str += f.slice(lastPos, i); - lastPos = i + 1; } } - } - if (lastPos === 0) - str = f; - else if (lastPos < f.length) - str += f.slice(lastPos); - while (a < arguments.length) { - const x = arguments[a++]; - if ((typeof x !== 'object' && typeof x !== 'symbol') || x === null) { - str += ` ${x}`; - } else { - str += ` ${inspect(x, inspectOptions)}`; + if (lastPos === 0) { + str = first; + } else if (lastPos < first.length) { + str += first.slice(lastPos); + } + + parts.push(str); + while (a < args.length) { + parts.push(formatValue(args[a], inspectOptions)); + a++; + } + } else { + for (const arg of args) { + parts.push(formatValue(arg, inspectOptions)); } } - return str; + + return parts.join(' '); } const debugs = {}; diff --git a/test/parallel/test-util-format.js b/test/parallel/test-util-format.js index 0c4ba82fec..2ca8e0857f 100644 --- a/test/parallel/test-util-format.js +++ b/test/parallel/test-util-format.js @@ -273,6 +273,10 @@ assert.strictEqual(util.format('percent: %d%, fraction: %d', 10, 0.1), 'percent: 10%, fraction: 0.1'); assert.strictEqual(util.format('abc%', 1), 'abc% 1'); +// Additional arguments after format specifiers +assert.strictEqual(util.format('%i', 1, 'number'), '1 number'); +assert.strictEqual(util.format('%i', 1, () => {}), '1 [Function]'); + { const o = {}; o.o = o; @@ -315,3 +319,15 @@ function BadCustomError(msg) { util.inherits(BadCustomError, Error); assert.strictEqual(util.format(new BadCustomError('foo')), '[BadCustomError: foo]'); + +// The format of arguments should not depend on type of the first argument +assert.strictEqual(util.format('1', '1'), '1 1'); +assert.strictEqual(util.format(1, '1'), '1 1'); +assert.strictEqual(util.format('1', 1), '1 1'); +assert.strictEqual(util.format(1, 1), '1 1'); +assert.strictEqual(util.format('1', () => {}), '1 [Function]'); +assert.strictEqual(util.format(1, () => {}), '1 [Function]'); +assert.strictEqual(util.format('1', "'"), "1 '"); +assert.strictEqual(util.format(1, "'"), "1 '"); +assert.strictEqual(util.format('1', 'number'), '1 number'); +assert.strictEqual(util.format(1, 'number'), '1 number'); |