Merge remote-tracking branch 'upstream/main'

This commit is contained in:
2026-08-26 09:31:34 +08:00
parent 9b8c4bfab6
commit 394eb0285d
34137 changed files with 3589424 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [ljharb]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: npm/shell-quote
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+14
View File
@@ -0,0 +1,14 @@
{
"all": true,
"check-coverage": false,
"reporter": ["text-summary", "text", "html", "json"],
"lines": 86,
"statements": 85.93,
"functions": 82.43,
"branches": 76.06,
"exclude": [
"coverage",
"example",
"test"
]
}
+24
View File
@@ -0,0 +1,24 @@
The MIT License
Copyright (c) 2013 James Halliday (mail@substack.net)
Permission is hereby granted, free of charge,
to any person obtaining a copy of this software and
associated documentation files (the "Software"), to
deal in the Software without restriction, including
without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom
the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+213
View File
@@ -0,0 +1,213 @@
# shell-quote <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
[![github actions][actions-image]][actions-url]
[![coverage][codecov-image]][codecov-url]
[![License][license-image]][license-url]
[![Downloads][downloads-image]][downloads-url]
[![npm badge][npm-badge-png]][package-url]
Parse and quote shell commands.
# example
## quote
```js
var quote = require('shell-quote/quote');
var s = quote([ 'a', 'b c d', '$f', '"g"' ]);
console.log(s);
```
output
```
a 'b c d' \$f '"g"'
```
## parse
```js
var parse = require('shell-quote/parse');
var xs = parse('a "b c" \\$def \'it\'\\\'\'s great\'');
console.dir(xs);
```
output
```
[ 'a', 'b c', '$def', "it's great" ]
```
## parse with an environment variable
```js
var parse = require('shell-quote/parse');
var xs = parse('beep --boop="$PWD"', { PWD: '/home/robot' });
console.dir(xs);
```
output
```
[ 'beep', '--boop=/home/robot' ]
```
## parse with custom escape character
```js
var parse = require('shell-quote/parse');
var xs = parse('beep ^--boop="$PWD"', { PWD: '/home/robot' }, { escape: '^' });
console.dir(xs);
```
output
```
[ 'beep', '--boop=/home/robot' ]
```
## parse with unquoted variable splitting
```js
var parse = require('shell-quote/parse');
var xs = parse('a $T', { T: 'c d' }, { splitUnquoted: true });
console.dir(xs);
```
output
```
[ 'a', 'c', 'd' ]
```
## parsing shell operators
```js
var parse = require('shell-quote/parse');
var xs = parse('beep || boop > /byte');
console.dir(xs);
```
output:
```
[ 'beep', { op: '||' }, 'boop', { op: '>' }, '/byte' ]
```
## parsing shell comment
```js
var parse = require('shell-quote/parse');
var xs = parse('beep > boop # > kaboom');
console.dir(xs);
```
output:
```
[ 'beep', { op: '>' }, 'boop', { comment: ' > kaboom' } ]
```
# methods
```js
var quote = require('shell-quote/quote');
var parse = require('shell-quote/parse');
```
## quote(args)
Return a quoted string for the array `args` suitable for using in shell
commands.
Each entry of `args` may be a string, or one of the object shapes that
`parse` emits: `{ op }` (where `op` is one of the control operators
`||`, `&&`, `;;`, `|&`, `<(`, `<<<`, `>>`, `>&`, `<&`, `&`, `;`, `(`,
`)`, `|`, `<`, `>`), `{ op: 'glob', pattern }`, or `{ comment }`. Any
other object shape, an unrecognized `op`, or a `pattern`/`comment`
containing line terminators throws a `TypeError`.
The output is POSIX shell (`sh`/`bash`) quoting.
It is not valid for Windows `cmd.exe` or PowerShell,
whose rules differ and, for `cmd.exe`,
are not solvable in the general case. On Windows,
do not build a shell command string from this output;
instead pass an argument array to a non-shell API such as
`child_process.execFile` or `spawn`
(or the `cross-spawn` package),
which does no shell parsing and needs no quoting.
Use the returned string verbatim as shell input.
It is already a complete, escaped shell word (or words);
do not wrap it in additional quotes or embed it in `eval '...'`.
Re-quoting the output (for example, placing it inside single quotes)
turns its backslash escapes into literal characters and corrupts the value.
## parse(cmd, env={})
Return an array of arguments from the quoted string `cmd`.
Interpolate embedded bash-style `$VARNAME` and `${VARNAME}` variables with
the `env` object which like bash will replace undefined variables with `""`.
By default an expanded variable is a single token even when unquoted.
Pass `{ splitUnquoted: true }` to split an unquoted expansion into multiple tokens the way a shell performs field splitting,
using the default `IFS` (space, tab, newline).
Pass a string to use its characters as the `IFS` instead
(for example `{ splitUnquoted: ':' }`).
A quoted expansion (`"$VAR"`) is never split.
Only simple `$VARNAME` and `${VARNAME}` interpolation is supported.
Bash parameter expansion beyond a plain variable name is not evaluated:
forms such as array subscripts (`${arr[i]}`), length (`${#arr[@]}`),
and modifiers (`${var:-default}`, `${var/a/b}`)
are treated as an unknown variable and expand to `""`,
while arithmetic (`$((...))`) and command substitution (`$(...)`)
are not interpreted.
Whitespace inside `${...}` throws a `Bad substitution` error.
`env` is usually an object but it can also be a function to perform lookups.
When `env(key)` returns a string, its result will be output just like `env[key]` would.
When `env(key)` returns an object, it will be inserted into the result
array like the operator objects.
When a bash operator is encountered,
the element in the array with be an object with an `"op"` key set to the operator string.
For example:
```
'beep || boop > /byte'
```
parses as:
```
[ 'beep', { op: '||' }, 'boop', { op: '>' }, '/byte' ]
```
# install
With [npm](http://npmjs.org) do:
```
npm install shell-quote
```
# license
MIT
[package-url]: https://npmjs.org/package/shell-quote
[npm-version-svg]: https://versionbadg.es/ljharb/shell-quote.svg
[deps-svg]: https://david-dm.org/ljharb/shell-quote.svg
[deps-url]: https://david-dm.org/ljharb/shell-quote
[npm-badge-png]: https://nodei.co/npm/shell-quote.png?downloads=true&stars=true
[license-image]: https://img.shields.io/npm/l/shell-quote.svg
[license-url]: LICENSE
[downloads-image]: https://img.shields.io/npm/dm/shell-quote.svg
[downloads-url]: https://npm-stat.com/charts.html?package=shell-quote
[codecov-image]: https://codecov.io/gh/ljharb/shell-quote/branch/main/graphs/badge.svg
[codecov-url]: https://app.codecov.io/gh/ljharb/shell-quote/
[actions-image]: https://img.shields.io/github/check-runs/ljharb/shell-quote/main
[actions-url]: https://github.com/ljharb/shell-quote/actions
+29
View File
@@ -0,0 +1,29 @@
import ljharb from '@ljharb/eslint-config/flat';
export default [
...ljharb,
{
rules: {
'array-bracket-newline': 'off',
complexity: 'off',
eqeqeq: 'warn',
'func-style': ['error', 'declaration'],
'max-depth': 'off',
'max-lines-per-function': 'off',
'max-statements': 'off',
'multiline-comment-style': 'off',
'no-extra-parens': 'off',
'no-lonely-if': 'warn',
'no-negated-condition': 'warn',
'no-param-reassign': 'warn',
'no-shadow': 'warn',
'no-template-curly-in-string': 'off',
},
},
{
files: ['example/**'],
rules: {
'no-console': 'off',
},
},
];
+28
View File
@@ -0,0 +1,28 @@
import quote = require('./quote');
import parse = require('./parse');
export { quote, parse };
export type ControlOperator = parse.ControlOperator;
export type GlobPattern = parse.GlobPattern;
export type Comment = parse.Comment;
export type ParseEntry = parse.ParseEntry;
export type ParseOptions = parse.ParseOptions;
type Join<T extends readonly string[], D extends string> = T extends readonly []
? ''
: T extends readonly [infer F extends string]
? F
: T extends readonly [infer F extends string, ...infer R extends string[]]
? `${F}${D}${Join<R, D>}`
: string;
declare global {
interface ReadonlyArray<T> {
join<This extends readonly string[], D extends string = ','>(this: This, separator?: D): Join<This, D>;
}
interface Array<T> {
join<This extends readonly string[], D extends string = ','>(this: This, separator?: D): Join<This, D>;
}
}
+4
View File
@@ -0,0 +1,4 @@
'use strict';
exports.quote = require('./quote');
exports.parse = require('./parse');
+76
View File
@@ -0,0 +1,76 @@
{
"name": "shell-quote",
"description": "quote and parse shell commands",
"version": "1.10.0",
"author": {
"name": "James Halliday",
"email": "mail@substack.net",
"url": "http://substack.net"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
},
"bugs": "https://github.com/ljharb/shell-quote/issues",
"homepage": "https://github.com/ljharb/shell-quote",
"keywords": [
"command",
"parse",
"quote",
"shell"
],
"license": "MIT",
"main": "index.js",
"types": "index.d.ts",
"repository": {
"type": "git",
"url": "http://github.com/ljharb/shell-quote.git"
},
"scripts": {
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublish": "not-in-publish || npm run prepublishOnly",
"prepublishOnly": "safe-publish-latest",
"prelint": "evalmd README.md",
"lint": "eslint --ext=js,mjs .",
"postlint": "tsc -p . && attw -P",
"pretest": "npm run lint",
"tests-only": "nyc tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "npx npm@'>=10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.5",
"@ljharb/eslint-config": "^22.2.3",
"@ljharb/tsconfig": "^0.3.2",
"auto-changelog": "^2.6.0",
"eslint": "^10.6.0",
"evalmd": "^0.0.20",
"in-publish": "^2.0.1",
"jiti": "^0.0.0",
"npmignore": "^0.3.5",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
"tape": "^5.10.2",
"typescript": "next"
},
"auto-changelog": {
"output": "CHANGELOG.md",
"template": "keepachangelog",
"unreleased": false,
"commitLimit": false,
"backfillLimit": false,
"hideCredit": true,
"startingVersion": "1.7.4"
},
"publishConfig": {
"ignore": [
".github/workflows",
"example",
"CHANGELOG.md"
]
},
"engines": {
"node": ">= 0.4"
}
}
+48
View File
@@ -0,0 +1,48 @@
declare namespace parse {
/** A shell control operator. */
export interface ControlOperator {
op: '||' | '&&' | ';;' | '|&' | '<(' | '<<<' | '>>' | '>&' | '<&' | '&' | ';' | '(' | ')' | '|' | '<' | '>';
}
/** A glob pattern parsed from the shell command. */
export interface GlobPattern {
op: 'glob';
pattern: string;
}
/** A shell comment. */
export interface Comment {
comment: string;
}
/** A parsed token returned by {@link parse}. */
export type ParseEntry = string | ControlOperator | GlobPattern | Comment;
/** Options for the {@link parse} function. */
export interface ParseOptions {
/** Custom escape character. Defaults to `\\`. */
escape?: string;
/** Field-splits an unquoted variable expansion, the way a shell does using `IFS`; quoted expansions are never split. `true` uses the default IFS (space, tab, newline); a string uses its characters as the IFS. An empty string, `false`, or omitting the option disables splitting. Defaults to false. */
splitUnquoted?: boolean | string;
}
export type Env =
| Record<string, string | undefined>
| ((key: string) => string | object | undefined);
}
/**
* Parses a shell command string into an array of tokens.
*
* @param s - The shell command string to parse.
* @param env - Optional environment variables for expansion, either as an object of string values or a lookup function. When the lookup function returns an object, that object is inserted into the result verbatim.
* @param opts - Optional parsing options.
* @returns An array of parsed tokens, including any objects returned by an `env` lookup function.
*/
declare function parse<T extends string | object = never>(
s: string,
env?: parse.Env,
opts?: parse.ParseOptions,
): (parse.ParseEntry | T)[];
export = parse;
+330
View File
@@ -0,0 +1,330 @@
'use strict';
/**
* @import {
* ControlOperator,
* Env,
* GlobPattern,
* ParseEntry,
* } from './parse' */
// '<(' is process substitution operator and
// can be parsed the same as control operator
var CONTROL = /** @type {const} */ ('(?:') + /** @type {const} */ ([
'\\|\\|',
'\\&\\&',
';;',
'\\|\\&',
'\\<\\(',
'\\<\\<\\<',
'>>',
'>\\&',
'<\\&',
'[&;()|<>]'
]).join(/** @type {const} */ ('|')) + /** @type {const} */ (')');
var controlRE = new RegExp('^' + CONTROL + '$');
var META = /** @type {const} */ ('|&;()<> \\t');
var SINGLE_QUOTE = /** @type {const} */ ('\'([^\']*?)\'');
var DOUBLE_QUOTE = /** @type {const} */ ('"((\\\\"|[^"])*?)"');
var hash = /^#$/;
var SQ = /** @type {const} */ ("'");
var DQ = /** @type {const} */ ('"');
var DS = /** @type {const} */ ('$');
var TOKEN = '';
var mult = /** @type {const} */ (0x100000000); // Math.pow(16, 8);
for (var i = 0; i < 4; i++) {
TOKEN += (mult * Math.random()).toString(16);
}
var startsWithToken = new RegExp('^' + TOKEN);
/**
* @param {string} s
* @param {RegExp} r
*/
function matchAll(s, r) {
var origIndex = r.lastIndex;
var matches = [];
var matchObj;
while ((matchObj = r.exec(s))) {
matches[matches.length] = matchObj;
if (r.lastIndex === matchObj.index) {
r.lastIndex += 1;
}
}
r.lastIndex = origIndex;
return matches;
}
/**
* @param {Env} env
* @param {string} pre
* @param {string} key
*/
function getVar(env, pre, key) {
var r = typeof env === 'function' ? env(key) : env[key];
if (typeof r === 'undefined' && key != '') {
r = '';
} else if (typeof r === 'undefined') {
r = '$';
}
if (typeof r === 'object') {
return pre + TOKEN + JSON.stringify(r) + TOKEN;
}
return pre + r;
}
/**
* @param {string} string
* @param {Env} [env]
* @param {{ escape?: string, splitUnquoted?: boolean | string }} [opts]
* @returns {ParseEntry[]}
*/
function parseInternal(string, env, opts) {
if (!opts) {
opts = {};
}
var BS = opts.escape || '\\';
var ifs = opts.splitUnquoted === true ? ' \t\n' : (typeof opts.splitUnquoted === 'string' ? opts.splitUnquoted : '');
var BAREWORD = '(\\' + BS + '[\'"' + META + ']|[^\\s\'"' + META + '])+';
var chunker = new RegExp([
'(' + CONTROL + ')', // control chars
'(' + BAREWORD + '|' + DOUBLE_QUOTE + '|' + SINGLE_QUOTE + ')+'
].join('|'), 'g');
var matches = matchAll(string, chunker);
if (matches.length === 0) {
return [];
}
if (!env) {
env = {};
}
var commented = false;
return matches.map(function (match) {
var s = match[0];
if (!s || commented) {
return void undefined;
}
if (controlRE.test(s)) {
return /** @type {ControlOperator} */ ({ op: s });
}
// Hand-written scanner/parser for Bash quoting rules:
//
// 1. inside single quotes, all characters are printed literally.
// 2. inside double quotes, all characters are printed literally
// except variables prefixed by '$' and backslashes followed by
// either a double quote or another backslash.
// 3. outside of any quotes, backslashes are treated as escape
// characters and not printed (unless they are themselves escaped)
// 4. quote context can switch mid-token if there is no whitespace
// between the two quote contexts (e.g. all'one'"token" parses as
// "allonetoken")
/** @type {string | boolean} */
var quote = false;
var esc = false;
var out = '';
/** @type {string[]} */
var words = [];
var sawQuote = false;
/** @type {number | null} */
var pendingNw = null;
var isGlob = false;
/** @type {number} */
var i;
function parseEnvVar() {
i += 1;
/** @type {number | RegExpMatchArray | null} */
var varend;
/** @type {string} */
var varname;
var char = s.charAt(i);
if (char === '{') {
i += 1;
if (s.charAt(i) === '}') {
throw new Error('Bad substitution: ' + s.slice(i - 2, i + 1));
}
// match braces by depth so a nested `${` keeps its inner `}` from ending the outer substitution
var depth = 1;
varend = i;
while (depth > 0 && varend < s.length) {
if (s.charAt(varend) === '{' && s.charAt(varend - 1) === '$') {
depth += 1;
} else if (s.charAt(varend) === '}') {
depth -= 1;
}
varend += 1;
}
if (depth !== 0) {
throw new Error('Bad substitution: ' + s.slice(i));
}
varend -= 1;
varname = s.slice(i, varend);
i = varend;
} else if ((/[*@#?$!_-]/).test(char)) {
varname = char;
i += 1;
} else {
var slicedFromI = s.slice(i);
varend = slicedFromI.match(/[^\w\d_]/);
if (!varend) {
varname = slicedFromI;
i = s.length;
} else {
varname = slicedFromI.slice(0, varend.index);
i += /** @type {number} */ (varend.index) - 1;
}
}
return getVar(/** @type {NonNullable<typeof env>} */ (env), '', varname);
}
function flushRun() {
if (pendingNw === null) {
return;
}
if (pendingNw === 0) {
if (out !== '') {
words[words.length] = out;
out = '';
}
} else {
words[words.length] = out;
out = '';
for (var fe = 1; fe < pendingNw; fe += 1) {
words[words.length] = '';
}
}
pendingNw = null;
}
for (i = 0; i < s.length; i++) {
var c = s.charAt(i);
if (ifs && c !== DS) {
flushRun();
}
isGlob = isGlob || (!quote && (c === '*' || c === '?'));
if (esc) {
out += c;
esc = false;
} else if (quote) {
if (c === quote) {
quote = false;
} else if (quote == SQ) {
out += c;
} else { // Double quote
if (c === BS) {
i += 1;
c = s.charAt(i);
if (c === DQ || c === BS || c === DS) {
out += c;
} else {
out += BS + c;
}
} else if (c === DS) {
out += parseEnvVar();
} else {
out += c;
}
}
} else if (c === DQ || c === SQ) {
quote = c;
sawQuote = true;
} else if (controlRE.test(c)) {
return /** @type {ControlOperator} */ ({ op: s });
} else if (hash.test(c)) {
commented = true;
var commentObj = { comment: string.slice(match.index + i + 1) };
if (out.length) {
return /** @type {const} */ ([out, commentObj]);
}
return /** @type {const} */ ([commentObj]);
} else if (c === BS) {
esc = true;
} else if (c === DS) {
var value = parseEnvVar();
if (!ifs) {
out += value;
} else {
for (var vi = 0; vi < value.length; vi += 1) {
var vc = value.charAt(vi);
if (ifs.indexOf(vc) < 0) {
flushRun();
out += vc;
} else if (pendingNw === null) {
pendingNw = vc === ' ' || vc === '\t' || vc === '\n' ? 0 : 1;
} else if (vc !== ' ' && vc !== '\t' && vc !== '\n') {
pendingNw += 1;
}
}
}
} else {
out += c;
}
}
if (isGlob) {
return /** @type {GlobPattern} */ ({ op: 'glob', pattern: out });
}
if (ifs) {
if (pendingNw !== null && pendingNw > 0) {
words[words.length] = out;
out = '';
for (var te = 1; te < pendingNw; te += 1) {
words[words.length] = '';
}
}
if (out !== '' || (sawQuote && words.length === 0)) {
words[words.length] = out;
}
return words;
}
return out;
}).reduce(function (prev, arg) { // finalize parsed arguments
if (typeof arg === 'undefined') {
return prev;
}
/** @type {ParseEntry[]} */ ([]).concat(arg).forEach(function (entry) {
prev[prev.length] = entry;
});
return prev;
}, /** @type {ParseEntry[]} */ ([]));
}
/** @type {typeof import('./parse')} */
module.exports = function parse(s, env, opts) {
var mapped = parseInternal(s, env, opts);
if (typeof env !== 'function') {
return mapped;
}
return mapped.reduce(function (acc, s) {
if (typeof s === 'object') {
acc[acc.length] = s;
return acc;
}
var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g'));
if (xs.length === 1) {
acc[acc.length] = xs[0];
return acc;
}
xs.filter(Boolean).forEach(function (x) {
acc[acc.length] = startsWithToken.test(x)
? JSON.parse(x.split(TOKEN)[1])
: x;
});
return acc;
}, /** @type {ParseEntry[]} */ ([]));
};
+15
View File
@@ -0,0 +1,15 @@
import parse = require('./parse');
/**
* Quotes an array of tokens into a shell-safe string.
*
* Accepts strings and the object shapes that {@link parse} emits. Throws a
* `TypeError` for unrecognized object shapes, `op` values outside the
* allowlist, or `pattern`/`comment` values containing line terminators.
*
* @param args - Array of tokens to quote.
* @returns A shell-safe quoted string.
*/
declare function quote(args: readonly parse.ParseEntry[]): string;
export = quote;
+65
View File
@@ -0,0 +1,65 @@
'use strict';
/** @import { ControlOperator } from './parse' */
/** @type {ControlOperator['op'][]} */
var OPS = /** @type {const} */ ([
'||',
'&&',
';;',
'|&',
'<(',
'<<<',
'>>',
'>&',
'<&',
'&',
';',
'(',
')',
'|',
'<',
'>'
]);
var LINE_TERMINATORS = /[\n\r\u2028\u2029]/;
var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|]/g;
/** @type {typeof import('./quote')} */
module.exports = function quote(xs) {
return xs.map(function (s) {
if (s === '') {
return /** @type {const} */ ('\'\'');
}
if (s && typeof s === 'object') {
if ('op' in s && s.op === 'glob') {
if (typeof s.pattern !== 'string') {
throw new TypeError('glob token requires a string `pattern`');
}
if (LINE_TERMINATORS.test(s.pattern)) {
throw new TypeError('glob `pattern` must not contain line terminators');
}
return s.pattern.replace(GLOB_SHELL_SPECIAL, '\\$&');
}
if ('op' in s && typeof s.op === 'string') {
if (OPS.indexOf(s.op) < 0) {
throw new TypeError('invalid `op` value: ' + JSON.stringify(s.op));
}
return s.op.replace(/[\s\S]/g, '\\$&');
}
if ('comment' in s && typeof s.comment === 'string') {
if (LINE_TERMINATORS.test(s.comment)) {
throw new TypeError('`comment` must not contain line terminators');
}
return '#' + s.comment;
}
throw new TypeError('unrecognized object token shape');
}
if ((/["\s\\]/).test(s) && !(/'/).test(s)) {
return "'" + s.replace(/(['])/g, '\\$1') + "'";
}
if ((/["'\s]/).test(s)) {
return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"';
}
return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}~])/g, '$1\\$2');
}).join(' ');
};
+11
View File
@@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
Only the latest major version is supported at any given time.
## Reporting a Vulnerability
To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.
+16
View File
@@ -0,0 +1,16 @@
'use strict';
var test = require('tape');
var parse = require('../').parse;
test('comment', function (t) {
t.same(parse('beep#boop'), ['beep', { comment: 'boop' }]);
t.same(parse('beep #boop'), ['beep', { comment: 'boop' }]);
t.same(parse('beep # boop'), ['beep', { comment: ' boop' }]);
t.same(parse('beep # > boop'), ['beep', { comment: ' > boop' }]);
t.same(parse('beep # "> boop"'), ['beep', { comment: ' "> boop"' }]);
t.same(parse('beep "#"'), ['beep', '#']);
t.same(parse('beep #"#"#'), ['beep', { comment: '"#"#' }]);
t.same(parse('beep > boop # > foo'), ['beep', { op: '>' }, 'boop', { comment: ' > foo' }]);
t.end();
});
+52
View File
@@ -0,0 +1,52 @@
'use strict';
var test = require('tape');
var parse = require('../').parse;
test('expand environment variables', function (t) {
t.same(parse('a $XYZ c', { XYZ: 'b' }), ['a', 'b', 'c']);
t.same(parse('a${XYZ}c', { XYZ: 'b' }), ['abc']);
t.same(parse('a${XYZ}c $XYZ', { XYZ: 'b' }), ['abc', 'b']);
t.same(parse('"-$X-$Y-"', { X: 'a', Y: 'b' }), ['-a-b-']);
t.same(parse("'-$X-$Y-'", { X: 'a', Y: 'b' }), ['-$X-$Y-']);
t.same(parse('qrs"$zzz"wxy', { zzz: 'tuv' }), ['qrstuvwxy']);
t.same(parse("qrs'$zzz'wxy", { zzz: 'tuv' }), ['qrs$zzzwxy']);
t.same(parse('qrs${zzz}wxy'), ['qrswxy']);
t.same(parse('qrs$wxy $'), ['qrs', '$']);
t.same(parse('grep "xy$"'), ['grep', 'xy$']);
t.same(parse('ab$x', { x: 'c' }), ['abc']);
t.same(parse('ab\\$x', { x: 'c' }), ['ab$x']);
t.same(parse('ab${x}def', { x: 'c' }), ['abcdef']);
t.same(parse('ab\\${x}def', { x: 'c' }), ['ab${x}def']);
t.same(parse('"ab\\${x}def"', { x: 'c' }), ['ab${x}def']);
t.end();
});
test('expand environment variables within here-strings', function (t) {
t.same(parse('a <<< $x', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']);
t.same(parse('a <<< ${x}', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']);
t.same(parse('a <<< "$x"', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']);
t.same(parse('a <<< "${x}"', { x: 'Joe' }), ['a', { op: '<<<' }, 'Joe']);
t.end();
});
test('environment variables with metacharacters', function (t) {
t.same(parse('a $XYZ c', { XYZ: '"b"' }), ['a', '"b"', 'c']);
t.same(parse('a $XYZ c', { XYZ: '$X', X: 5 }), ['a', '$X', 'c']);
t.same(parse('a"$XYZ"c', { XYZ: "'xyz'" }), ["a'xyz'c"]);
t.end();
});
test('special shell parameters', function (t) {
var chars = '*@#?-$!0_'.split('');
t.plan(chars.length);
chars.forEach(function (c) {
var env = {};
env[c] = 'xxx';
t.same(parse('a $' + c + ' c', env), ['a', 'xxx', 'c']);
});
});
+21
View File
@@ -0,0 +1,21 @@
'use strict';
var test = require('tape');
var parse = require('../').parse;
function getEnv() {
return 'xxx';
}
function getEnvObj() {
return { op: '@@' };
}
test('functional env expansion', function (t) {
t.plan(4);
t.same(parse('a $XYZ c', getEnv), ['a', 'xxx', 'c']);
t.same(parse('a $XYZ c', getEnvObj), ['a', { op: '@@' }, 'c']);
t.same(parse('a${XYZ}c', getEnvObj), ['a', { op: '@@' }, 'c']);
t.same(parse('"a $XYZ c"', getEnvObj), ['a ', { op: '@@' }, ' c']);
});
+102
View File
@@ -0,0 +1,102 @@
'use strict';
var test = require('tape');
var parse = require('../').parse;
test('single operators', function (t) {
t.same(parse('beep | boop'), ['beep', { op: '|' }, 'boop']);
t.same(parse('beep|boop'), ['beep', { op: '|' }, 'boop']);
t.same(parse('beep \\| boop'), ['beep', '|', 'boop']);
t.same(parse('beep "|boop"'), ['beep', '|boop']);
t.same(parse('echo zing &'), ['echo', 'zing', { op: '&' }]);
t.same(parse('echo zing&'), ['echo', 'zing', { op: '&' }]);
t.same(parse('echo zing\\&'), ['echo', 'zing&']);
t.same(parse('echo "zing\\&"'), ['echo', 'zing\\&']);
t.same(parse('beep;boop'), ['beep', { op: ';' }, 'boop']);
t.same(parse('(beep;boop)'), [
{ op: '(' }, 'beep', { op: ';' }, 'boop', { op: ')' }
]);
t.same(parse('beep>boop'), ['beep', { op: '>' }, 'boop']);
t.same(parse('beep 2>boop'), ['beep', '2', { op: '>' }, 'boop']);
t.same(parse('beep<boop'), ['beep', { op: '<' }, 'boop']);
t.end();
});
test('double operators', function (t) {
t.same(parse('beep || boop'), ['beep', { op: '||' }, 'boop']);
t.same(parse('beep||boop'), ['beep', { op: '||' }, 'boop']);
t.same(parse('beep ||boop'), ['beep', { op: '||' }, 'boop']);
t.same(parse('beep|| boop'), ['beep', { op: '||' }, 'boop']);
t.same(parse('beep || boop'), ['beep', { op: '||' }, 'boop']);
t.same(parse('beep && boop'), ['beep', { op: '&&' }, 'boop']);
t.same(
parse('beep && boop || byte'),
['beep', { op: '&&' }, 'boop', { op: '||' }, 'byte']
);
t.same(
parse('beep&&boop||byte'),
['beep', { op: '&&' }, 'boop', { op: '||' }, 'byte']
);
t.same(
parse('beep\\&\\&boop||byte'),
['beep&&boop', { op: '||' }, 'byte']
);
t.same(
parse('beep\\&&boop||byte'),
['beep&', { op: '&' }, 'boop', { op: '||' }, 'byte']
);
t.same(
parse('beep;;boop|&byte>>blip'),
['beep', { op: ';;' }, 'boop', { op: '|&' }, 'byte', { op: '>>' }, 'blip']
);
t.same(parse('beep 2>&1'), ['beep', '2', { op: '>&' }, '1']);
t.same(
parse('beep<(boop)'),
['beep', { op: '<(' }, 'boop', { op: ')' }]
);
t.same(
parse('beep<<(boop)'),
['beep', { op: '<' }, { op: '<(' }, 'boop', { op: ')' }]
);
t.end();
});
test('duplicating input file descriptors', function (t) {
// duplicating stdout to file descriptor 3
t.same(parse('beep 3<&1'), ['beep', '3', { op: '<&' }, '1']);
// duplicating stdout to file descriptor 0, i.e. stdin
t.same(parse('beep <&1'), ['beep', { op: '<&' }, '1']);
// closes stdin
t.same(parse('beep <&-'), ['beep', { op: '<&' }, '-']);
t.end();
});
test('here strings', function (t) {
t.same(parse('cat <<< "hello world"'), ['cat', { op: '<<<' }, 'hello world']);
t.same(parse('cat <<< hello'), ['cat', { op: '<<<' }, 'hello']);
t.same(parse('cat<<<hello'), ['cat', { op: '<<<' }, 'hello']);
t.same(parse('cat<<<"hello world"'), ['cat', { op: '<<<' }, 'hello world']);
t.end();
});
test('glob patterns', function (t) {
t.same(
parse('tap test/*.test.js'),
['tap', { op: 'glob', pattern: 'test/*.test.js' }]
);
t.same(parse('tap "test/*.test.js"'), ['tap', 'test/*.test.js']);
t.end();
});
+149
View File
@@ -0,0 +1,149 @@
'use strict';
var test = require('tape');
var parse = require('../').parse;
var quote = require('../').quote;
test('parse shell commands', function (t) {
t.same(parse(''), [], 'parses an empty string');
t['throws'](
function () { parse('${}'); },
Error,
'empty substitution throws'
);
t['throws'](
function () { parse('${'); },
Error,
'incomplete substitution throws'
);
t.same(parse('a \'b\' "c"'), ['a', 'b', 'c']);
t.same(
parse('beep "boop" \'foo bar baz\' "it\'s \\"so\\" groovy"'),
['beep', 'boop', 'foo bar baz', 'it\'s "so" groovy']
);
t.same(parse('a b\\ c d'), ['a', 'b c', 'd']);
t.same(parse('\\$beep bo\\`op'), ['$beep', 'bo`op']);
t.same(parse('echo "foo = \\"foo\\""'), ['echo', 'foo = "foo"']);
t.same(parse(''), []);
t.same(parse(' '), []);
t.same(parse('\t'), []);
t.same(parse('a"b c d"e'), ['ab c de']);
t.same(parse('a\\ b"c d"\\ e f'), ['a bc d e', 'f']);
t.same(parse('a\\ b"c d"\\ e\'f g\' h'), ['a bc d ef g', 'h']);
t.same(parse("x \"bl'a\"'h'"), ['x', "bl'ah"]);
// inside single quotes everything is literal, so a backslash does not escape
// the closing quote and a quoted token must end at its first closing quote
t.same(parse("'\\' '\\'"), ['\\', '\\'], 'single-quoted backslashes parse as two separate tokens');
t.same(parse("'\\'\\''"), ["\\'"], 'single-quoted backslash joined with an escaped quote');
t.same(parse(quote(['\\', '\\'])), ['\\', '\\'], 'quote/parse round-trips a pair of backslashes');
t.same(parse("x bl^'a^'h'", {}, { escape: '^' }), ['x', "bl'a'h"]);
t.same(parse('abcH def', {}, { escape: 'H' }), ['abc def']);
t.deepEqual(parse('# abc def ghi'), [{ comment: ' abc def ghi' }], 'start-of-line comment content is unparsed');
t.deepEqual(parse('xyz # abc def ghi'), ['xyz', { comment: ' abc def ghi' }], 'comment content is unparsed');
t.deepEqual(parse('-x "" -y'), ['-x', '', '-y'], 'empty string is preserved');
t.same(
parse('2;b', {}, { escape: 'd' }),
[{ op: '2;b' }],
'control char in unquoted context mid-token with regex-special escape returns op'
);
t.end();
});
test('single quotes are literal', function (t) {
t.same(parse("'\\'\\'"), ["\\'"], 'close-escape-reopen produces a quote after a quoted backslash');
t.same(parse("'a'\\''b'"), ["a'b"], 'close-escape-reopen embeds a quote mid-word');
t.same(parse("'\\'x"), ['\\x'], 'bareword joins a preceding quoted backslash');
t.same(parse("a'\\'b"), ['a\\b'], 'quoted backslash joins surrounding barewords');
t.same(parse("''"), [''], 'empty single quotes produce an empty token');
t.same(parse("''a''"), ['a'], 'empty single quotes join adjacent content');
t.same(parse("'*'"), ['*'], 'quoted glob char is a plain string, not a glob');
t.end();
});
test('unmatched single quotes', function (t) {
// real shells reject unterminated quotes; parse is lenient, and these pin the shape of that leniency
t.same(parse("'"), [], 'a lone quote is dropped');
t.same(parse("'a"), ['a'], 'an unterminated quote keeps its content');
t.same(parse("a'b"), ['a', 'b'], 'an unmatched quote mid-word splits the token');
t.end();
});
test('nested parameter expansion', function (t) {
t.same(parse('${a${b}c}'), [''], 'a nested ${} is consumed as one substitution, not split at the first }');
t.same(parse('${a${b}}'), [''], 'a nested ${} at the end is consumed as one substitution');
t.same(parse('${foo{bar}'), [''], 'a lone { that is not part of a nested ${} does not change brace depth');
t.same(
parse('level=${levels[$RANDOM%${#levels[@]}]}'),
['level='],
'a nested array-index expansion is consumed whole, without leaking a partial token'
);
t.end();
});
test('splitUnquoted: field-splits unquoted variable expansion (#1)', function (t) {
var env = { T: 'c d', E: '', S: ' c d ', W: ' ' };
var opts = { splitUnquoted: true };
t.same(parse('test a b $T', env, opts), ['test', 'a', 'b', 'c', 'd'], 'unquoted expansion splits into separate tokens');
t.same(parse('a$T', env, opts), ['ac', 'd'], 'the first field joins the preceding text');
t.same(parse('$T x', env, opts), ['c', 'd', 'x'], 'the last field is a token of its own');
t.same(parse('x${T}y', env, opts), ['xc', 'dy'], 'fields join text on both sides');
t.same(parse('$S', env, opts), ['c', 'd'], 'leading, trailing, and repeated whitespace collapses');
t.same(parse('a$S', env, opts), ['a', 'c', 'd'], 'leading whitespace closes the preceding field');
t.same(parse('$W', env, opts), [], 'an all-whitespace expansion produces no tokens');
t.same(parse('a$W b', env, opts), ['a', 'b'], 'an all-whitespace expansion just separates fields');
t.same(parse('$E', env, opts), [], 'an empty unquoted expansion produces no token');
t.same(parse('"$T"', env, opts), ['c d'], 'a quoted expansion is never split');
t.same(parse('-x "" -y', env, opts), ['-x', '', '-y'], 'a quoted empty string is still preserved');
t.same(parse('a $F b', function () { return 'c d'; }, opts), ['a', 'c', 'd', 'b'], 'the env-function path splits too');
t.same(parse('test a b $T', env), ['test', 'a', 'b', 'c d'], 'without the option, unquoted expansion is not split');
t.end();
});
test('splitUnquoted: a string value is a custom IFS (#1)', function (t) {
function o(ifs) { return { splitUnquoted: ifs }; }
t.same(parse('${V}', { V: 'a:b' }, o(':')), ['a', 'b'], 'a non-whitespace IFS char splits fields');
t.same(parse('${V}', { V: 'a::b' }, o(':')), ['a', '', 'b'], 'adjacent non-whitespace delimiters yield an empty field');
t.same(parse('${V}', { V: ':a:' }, o(':')), ['', 'a'], 'a leading delimiter yields a leading empty; a trailing one does not');
t.same(parse('${V}', { V: 'a::' }, o(':')), ['a', ''], 'a trailing double delimiter yields one empty field');
t.same(parse('${V}', { V: ':' }, o(':')), [''], 'a lone delimiter yields a single empty field');
t.same(parse('${V}', { V: '::' }, o(':')), ['', ''], 'two delimiters yield two empty fields');
t.same(parse('${V}${W}', { V: 'a:', W: ':b' }, o(':')), ['a', '', 'b'], 'delimiters spanning an expansion boundary merge into one run');
t.same(parse('a${V}${W}z', { V: ':x:', W: ':y:' }, o(':')), ['a', 'x', '', 'y', 'z'], 'fields join literal text on both sides across expansions');
t.same(parse('${V}', { V: 'a : b' }, o(' :')), ['a', 'b'], 'whitespace around a non-whitespace delimiter is absorbed');
t.same(parse('${V}', { V: 'a b:c' }, o(' :')), ['a', 'b', 'c'], 'mixed IFS: whitespace and non-whitespace each delimit');
t.same(parse('${V}', { V: 'a,b' }, o(',')), ['a', 'b'], 'any character can be the IFS');
t.same(parse('${V}', { V: 'a b' }, o('')), ['a b'], 'an empty IFS string disables splitting');
t.end();
});
test('parse stays linear in token count (GHSA-395f-4hp3-45gv)', function (t) {
// the old concat-in-reduce finalizer was O(n^2): this many tokens took
// ~minutes, so under the unfixed code this test hangs rather than passes
var n = 2e5;
var input = new Array(n + 1).join('x '); // avoid String#repeat for old engines
var words = parse(input);
t.equal(words.length, n, 'every token is returned');
t.equal(words[0], 'x', 'first token is correct');
t.equal(words[n - 1], 'x', 'last token is correct');
var withEnv = parse(input, function () { return 'v'; });
t.equal(withEnv.length, n, 'env-function path returns every token');
t.end();
});
+145
View File
@@ -0,0 +1,145 @@
'use strict';
var test = require('tape');
var quote = require('../').quote;
test('quote', function (t) {
t.equal(quote(['a', 'b', 'c d']), 'a b \'c d\'');
t.equal(
quote(['a', 'b', "it's a \"neat thing\""]),
'a b "it\'s a \\"neat thing\\""'
);
t.equal(
quote(['$', '`', '\'']),
'\\$ \\` "\'"'
);
t.equal(quote([]), '');
t.equal(quote(['a\nb']), "'a\nb'");
t.equal(quote([' #(){}*|][!']), "' #(){}*|][!'");
t.equal(quote(["'#(){}*|][!"]), '"\'#(){}*|][\\!"');
t.equal(quote(['X#(){}*|][!']), 'X\\#\\(\\)\\{\\}\\*\\|\\]\\[\\!');
t.equal(quote(['a\n#\nb']), "'a\n#\nb'");
t.equal(quote(['><;{}']), '\\>\\<\\;\\{\\}');
t.equal(quote(['a', 1, true, false]), 'a 1 true false');
t.equal(quote(['a', 1, null, undefined]), 'a 1 null undefined');
t.equal(quote(['a\\x']), "'a\\x'");
t.equal(quote(['a"b']), '\'a"b\'');
t.equal(quote(['"a"b"']), '\'"a"b"\'');
t.equal(quote(['a\\"b']), '\'a\\"b\'');
t.equal(quote(['a\\b']), '\'a\\b\'');
t.end();
});
test('quote tilde (escapes every ~ to prevent shell tilde-expansion)', function (t) {
t.equal(quote(['~']), '\\~');
t.equal(quote(['~/foo']), '\\~/foo');
t.equal(quote(['~root']), '\\~root');
t.equal(quote(['~root/x']), '\\~root/x');
t.equal(quote(['~+']), '\\~+');
t.equal(quote(['~-']), '\\~-');
t.equal(quote(['a~b']), 'a\\~b');
t.equal(quote(['x~']), 'x\\~');
t.end();
});
test('backslash with whitespace is not doubled in single quotes (#14)', function (t) {
t.equal(quote(['foo \\ bar']), "'foo \\ bar'", 'a backslash between spaces stays a single literal backslash');
t.equal(quote(['foo \\\\ bar']), "'foo \\\\ bar'", 'a double backslash is preserved, not quadrupled');
t.equal(quote(['foo\\\nbar']), "'foo\\\nbar'", 'a backslash before a newline is preserved');
t.end();
});
test('escapes shell-special characters conservatively (issue #11)', function (t) {
t.equal(quote(['make', 'CFLAGS=-DRELEASE']), 'make CFLAGS\\=-DRELEASE', 'escapes = so a leading word is not read as an assignment');
t.equal(quote(['a@b']), 'a\\@b', 'escapes @ (zsh globbing)');
t.equal(quote(['a^b']), 'a\\^b', 'escapes ^ (zsh extendedglob, csh)');
t.equal(quote(['a:b']), 'a\\:b', 'escapes :');
t.equal(quote(['a,b']), 'a\\,b', 'escapes , (brace expansion)');
t.equal(quote(['a!b']), 'a\\!b', 'escapes ! (history expansion / pipeline negation)');
t.end();
});
test('quote ops', function (t) {
t.equal(quote(['a', { op: '|' }, 'b']), 'a \\| b');
t.equal(
quote(['a', { op: '&&' }, 'b', { op: ';' }, 'c']),
'a \\&\\& b \\; c'
);
t.end();
});
test('quote windows paths', { skip: 'breaking change, disabled until 2.x' }, function (t) {
var path = 'C:\\projects\\node-shell-quote\\index.js';
t.equal(quote([path, 'b', 'c d']), 'C:\\projects\\node-shell-quote\\index.js b \'c d\'');
t.end();
});
test("chars for windows paths don't break out", function (t) {
var x = '`:\\a\\b';
t.equal(quote([x]), "'`:\\a\\b'");
t.end();
});
test('empty strings', function (t) {
t.equal(quote(['-x', '', 'y']), '-x \'\' y');
t.end();
});
test('quote ops: allowlist', function (t) {
var ops = ['||', '&&', ';;', '|&', '<(', '<<<', '>>', '>&', '<&', '&', ';', '(', ')', '|', '<', '>'];
for (var i = 0; i < ops.length; i++) {
var op = ops[i];
var expected = '';
for (var j = 0; j < op.length; j++) { expected += '\\' + op.charAt(j); }
t.equal(quote([{ op: op }]), expected, 'op ' + op);
}
t.end();
});
test('quote ops: rejects line terminators (GHSA-w7jw-789q-3m8p)', function (t) {
t['throws'](function () { quote([{ op: ';\nid' }]); }, TypeError, 'newline in op');
t['throws'](function () { quote([{ op: ';\rid' }]); }, TypeError, 'carriage return in op');
t['throws'](function () { quote([{ op: ';\u2028id' }]); }, TypeError, 'U+2028 in op');
t['throws'](function () { quote([{ op: ';\u2029id' }]); }, TypeError, 'U+2029 in op');
t.end();
});
test('quote ops: rejects non-allowlisted values', function (t) {
t['throws'](function () { quote([{ op: '' }]); }, TypeError, 'empty op');
t['throws'](function () { quote([{ op: 'foo' }]); }, TypeError, 'arbitrary string');
t['throws'](function () { quote([{ op: '|||' }]); }, TypeError, 'near-miss');
t['throws'](function () { quote([{ op: 42 }]); }, TypeError, 'non-string op');
t.end();
});
test('quote glob pattern', function (t) {
t.equal(quote([{ op: 'glob', pattern: 'test/*.test.js' }]), 'test/*.test.js');
t.equal(quote([{ op: 'glob', pattern: '?ab' }]), '?ab');
t.equal(quote([{ op: 'glob', pattern: '[ab]c' }]), '[ab]c');
t.equal(quote([{ op: 'glob', pattern: '{a,b}' }]), '{a,b}');
t.equal(quote([{ op: 'glob', pattern: 'my dir/*.txt' }]), 'my\\ dir/*.txt');
t.equal(quote([{ op: 'glob', pattern: 'a$b' }]), 'a\\$b');
t['throws'](function () { quote([{ op: 'glob' }]); }, TypeError, 'missing pattern');
t['throws'](function () { quote([{ op: 'glob', pattern: 'a\nb' }]); }, TypeError, 'newline in pattern');
t['throws'](function () { quote([{ op: 'glob', pattern: 'a\u2028b' }]); }, TypeError, 'U+2028 in pattern');
t.end();
});
test('quote comment', function (t) {
t.equal(quote(['echo', 'hi', { comment: ' a comment' }]), 'echo hi # a comment');
t.equal(quote([{ comment: '' }]), '#');
t['throws'](function () { quote([{ comment: 'a\nb' }]); }, TypeError, 'newline in comment');
t['throws'](function () { quote([{ comment: 'a\rb' }]); }, TypeError, 'CR in comment');
t['throws'](function () { quote([{ comment: 'a\u2028b' }]); }, TypeError, 'U+2028 in comment');
t.end();
});
test('quote rejects unrecognized object shapes', function (t) {
t['throws'](function () { quote([{}]); }, TypeError, 'empty object');
t['throws'](function () { quote([{ foo: 'bar' }]); }, TypeError, 'unknown key');
t['throws'](function () { quote([{ op: null }]); }, TypeError, 'null op');
t.end();
});
+31
View File
@@ -0,0 +1,31 @@
'use strict';
var test = require('tape');
var parse = require('../').parse;
test('set env vars', function (t) {
t.same(
parse('ABC=444 x y z'),
['ABC=444', 'x', 'y', 'z']
);
t.same(
parse('ABC=3\\ 4\\ 5 x y z'),
['ABC=3 4 5', 'x', 'y', 'z']
);
t.same(
parse('X="7 8 9" printx'),
['X=7 8 9', 'printx']
);
t.same(
parse('X="7 8 9"; printx'),
['X=7 8 9', { op: ';' }, 'printx']
);
t.same(
parse('X="7 8 9"; printx', function () {
t.fail('should not have matched any keys');
}),
['X=7 8 9', { op: ';' }, 'printx']
);
t.end();
});
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "@ljharb/tsconfig",
"compilerOptions": {
"target": "ES2021"
},
"exclude": [
"coverage",
"test"
]
}