shell-quote
Advanced tools
| 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
| 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>; | ||
| } | ||
| } |
+46
| 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; | ||
| } | ||
| 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; |
+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; |
| { | ||
| "extends": "@ljharb/tsconfig", | ||
| "compilerOptions": { | ||
| "target": "ES2021" | ||
| }, | ||
| "exclude": [ | ||
| "coverage", | ||
| "test" | ||
| ] | ||
| } |
+18
-13
| { | ||
| "name": "shell-quote", | ||
| "description": "quote and parse shell commands", | ||
| "version": "1.8.4", | ||
| "version": "1.9.0", | ||
| "author": { | ||
@@ -14,14 +14,2 @@ "name": "James Halliday", | ||
| "bugs": "https://github.com/ljharb/shell-quote/issues", | ||
| "devDependencies": { | ||
| "@ljharb/eslint-config": "^22.2.3", | ||
| "auto-changelog": "^2.5.1", | ||
| "eslint": "^8.57.1", | ||
| "evalmd": "^0.0.19", | ||
| "in-publish": "^2.0.1", | ||
| "jackspeak": "=2.1.1", | ||
| "npmignore": "^0.3.5", | ||
| "nyc": "^10.3.2", | ||
| "safe-publish-latest": "^2.0.0", | ||
| "tape": "^5.9.0" | ||
| }, | ||
| "homepage": "https://github.com/ljharb/shell-quote", | ||
@@ -36,2 +24,3 @@ "keywords": [ | ||
| "main": "index.js", | ||
| "types": "index.d.ts", | ||
| "repository": { | ||
@@ -47,2 +36,3 @@ "type": "git", | ||
| "lint": "eslint --ext=js,mjs .", | ||
| "postlint": "tsc -p . && attw -P", | ||
| "pretest": "npm run lint", | ||
@@ -55,2 +45,17 @@ "tests-only": "nyc tape 'test/**/*.js'", | ||
| }, | ||
| "devDependencies": { | ||
| "@arethetypeswrong/cli": "^0.18.2", | ||
| "@ljharb/eslint-config": "^22.2.3", | ||
| "@ljharb/tsconfig": "^0.3.2", | ||
| "auto-changelog": "^2.6.0", | ||
| "eslint": "^10.5.0", | ||
| "evalmd": "^0.0.19", | ||
| "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": { | ||
@@ -57,0 +62,0 @@ "output": "CHANGELOG.md", |
+64
-29
| 'use strict'; | ||
| /** | ||
| * @import { | ||
| * ControlOperator, | ||
| * Env, | ||
| * GlobPattern, | ||
| * ParseEntry, | ||
| * } from './parse' */ | ||
| // '<(' is process substitution operator and | ||
| // can be parsed the same as control operator | ||
| var CONTROL = '(?:' + [ | ||
| var CONTROL = /** @type {const} */ ('(?:') + /** @type {const} */ ([ | ||
| '\\|\\|', | ||
@@ -16,15 +24,15 @@ '\\&\\&', | ||
| '[&;()|<>]' | ||
| ].join('|') + ')'; | ||
| ]).join(/** @type {const} */ ('|')) + /** @type {const} */ (')'); | ||
| var controlRE = new RegExp('^' + CONTROL + '$'); | ||
| var META = '|&;()<> \\t'; | ||
| var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"'; | ||
| var DOUBLE_QUOTE = '\'((\\\\\'|[^\'])*?)\''; | ||
| var META = /** @type {const} */ ('|&;()<> \\t'); | ||
| var SINGLE_QUOTE = /** @type {const} */ ('"((\\\\"|[^"])*?)"'); | ||
| var DOUBLE_QUOTE = /** @type {const} */ ('\'((\\\\\'|[^\'])*?)\''); | ||
| var hash = /^#$/; | ||
| var SQ = "'"; | ||
| var DQ = '"'; | ||
| var DS = '$'; | ||
| var SQ = /** @type {const} */ ("'"); | ||
| var DQ = /** @type {const} */ ('"'); | ||
| var DS = /** @type {const} */ ('$'); | ||
| var TOKEN = ''; | ||
| var mult = 0x100000000; // Math.pow(16, 8); | ||
| var mult = /** @type {const} */ (0x100000000); // Math.pow(16, 8); | ||
| for (var i = 0; i < 4; i++) { | ||
@@ -35,2 +43,6 @@ TOKEN += (mult * Math.random()).toString(16); | ||
| /** | ||
| * @param {string} s | ||
| * @param {RegExp} r | ||
| */ | ||
| function matchAll(s, r) { | ||
@@ -43,3 +55,3 @@ var origIndex = r.lastIndex; | ||
| while ((matchObj = r.exec(s))) { | ||
| matches.push(matchObj); | ||
| matches[matches.length] = matchObj; | ||
| if (r.lastIndex === matchObj.index) { | ||
@@ -55,2 +67,7 @@ r.lastIndex += 1; | ||
| /** | ||
| * @param {Env} env | ||
| * @param {string} pre | ||
| * @param {string} key | ||
| */ | ||
| function getVar(env, pre, key) { | ||
@@ -70,2 +87,8 @@ var r = typeof env === 'function' ? env(key) : env[key]; | ||
| /** | ||
| * @param {string} string | ||
| * @param {Env} [env] | ||
| * @param {{ escape?: string }} [opts] | ||
| * @returns {ParseEntry[]} | ||
| */ | ||
| function parseInternal(string, env, opts) { | ||
@@ -100,3 +123,3 @@ if (!opts) { | ||
| if (controlRE.test(s)) { | ||
| return { op: s }; | ||
| return /** @type {ControlOperator} */ ({ op: s }); | ||
| } | ||
@@ -115,2 +138,3 @@ | ||
| // "allonetoken") | ||
| /** @type {string | boolean} */ | ||
| var quote = false; | ||
@@ -120,2 +144,3 @@ var esc = false; | ||
| var isGlob = false; | ||
| /** @type {number} */ | ||
| var i; | ||
@@ -125,3 +150,5 @@ | ||
| i += 1; | ||
| /** @type {number | RegExpMatchArray | null} */ | ||
| var varend; | ||
| /** @type {string} */ | ||
| var varname; | ||
@@ -152,6 +179,6 @@ var char = s.charAt(i); | ||
| varname = slicedFromI.slice(0, varend.index); | ||
| i += varend.index - 1; | ||
| i += /** @type {number} */ (varend.index) - 1; | ||
| } | ||
| } | ||
| return getVar(env, '', varname); | ||
| return getVar(/** @type {NonNullable<typeof env>} */ (env), '', varname); | ||
| } | ||
@@ -188,3 +215,3 @@ | ||
| } else if (controlRE.test(c)) { | ||
| return { op: s }; | ||
| return /** @type {ControlOperator} */ ({ op: s }); | ||
| } else if (hash.test(c)) { | ||
@@ -194,5 +221,5 @@ commented = true; | ||
| if (out.length) { | ||
| return [out, commentObj]; | ||
| return /** @type {const} */ ([out, commentObj]); | ||
| } | ||
| return [commentObj]; | ||
| return /** @type {const} */ ([commentObj]); | ||
| } else if (c === BS) { | ||
@@ -208,3 +235,3 @@ esc = true; | ||
| if (isGlob) { | ||
| return { op: 'glob', pattern: out }; | ||
| return /** @type {GlobPattern} */ ({ op: 'glob', pattern: out }); | ||
| } | ||
@@ -214,7 +241,13 @@ | ||
| }).reduce(function (prev, arg) { // finalize parsed arguments | ||
| // TODO: replace this whole reduce with a concat | ||
| return typeof arg === 'undefined' ? prev : prev.concat(arg); | ||
| }, []); | ||
| if (typeof arg === 'undefined') { | ||
| return prev; | ||
| } | ||
| /** @type {ParseEntry[]} */ ([]).concat(arg).forEach(function (entry) { | ||
| prev[prev.length] = entry; | ||
| }); | ||
| return prev; | ||
| }, /** @type {ParseEntry[]} */ ([])); | ||
| } | ||
| /** @type {import('./parse')} */ | ||
| module.exports = function parse(s, env, opts) { | ||
@@ -227,15 +260,17 @@ var mapped = parseInternal(s, env, opts); | ||
| if (typeof s === 'object') { | ||
| return acc.concat(s); | ||
| acc[acc.length] = s; | ||
| return acc; | ||
| } | ||
| var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g')); | ||
| if (xs.length === 1) { | ||
| return acc.concat(xs[0]); | ||
| acc[acc.length] = xs[0]; | ||
| return acc; | ||
| } | ||
| return acc.concat(xs.filter(Boolean).map(function (x) { | ||
| if (startsWithToken.test(x)) { | ||
| return JSON.parse(x.split(TOKEN)[1]); | ||
| } | ||
| return x; | ||
| })); | ||
| }, []); | ||
| xs.filter(Boolean).forEach(function (x) { | ||
| acc[acc.length] = startsWithToken.test(x) | ||
| ? JSON.parse(x.split(TOKEN)[1]) | ||
| : x; | ||
| }); | ||
| return acc; | ||
| }, /** @type {ParseEntry[]} */ ([])); | ||
| }; |
+11
-7
| 'use strict'; | ||
| var OPS = [ | ||
| /** @import { ControlOperator } from './parse' */ | ||
| /** @type {ControlOperator['op'][]} */ | ||
| var OPS = /** @type {const} */ ([ | ||
| '||', | ||
@@ -20,13 +23,14 @@ '&&', | ||
| '>' | ||
| ]; | ||
| ]); | ||
| var LINE_TERMINATORS = /[\n\r\u2028\u2029]/; | ||
| var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|]/g; | ||
| /** @type {import('./quote')} */ | ||
| module.exports = function quote(xs) { | ||
| return xs.map(function (s) { | ||
| if (s === '') { | ||
| return '\'\''; | ||
| return /** @type {const} */ ('\'\''); | ||
| } | ||
| if (s && typeof s === 'object') { | ||
| if (s.op === 'glob') { | ||
| if ('op' in s && s.op === 'glob') { | ||
| if (typeof s.pattern !== 'string') { | ||
@@ -40,3 +44,3 @@ throw new TypeError('glob token requires a string `pattern`'); | ||
| } | ||
| if (typeof s.op === 'string') { | ||
| if ('op' in s && typeof s.op === 'string') { | ||
| if (OPS.indexOf(s.op) < 0) { | ||
@@ -47,3 +51,3 @@ throw new TypeError('invalid `op` value: ' + JSON.stringify(s.op)); | ||
| } | ||
| if (typeof s.comment === 'string') { | ||
| if ('comment' in s && typeof s.comment === 'string') { | ||
| if (LINE_TERMINATORS.test(s.comment)) { | ||
@@ -62,4 +66,4 @@ throw new TypeError('`comment` must not contain line terminators'); | ||
| } | ||
| return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$1\\$2'); | ||
| return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}~])/g, '$1\\$2'); | ||
| }).join(' '); | ||
| }; |
+17
-0
@@ -51,1 +51,18 @@ 'use strict'; | ||
| }); | ||
| 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(); | ||
| }); |
+12
-0
@@ -33,2 +33,14 @@ 'use strict'; | ||
| test('quote tilde (escapes leading ~ 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('quote ops', function (t) { | ||
@@ -35,0 +47,0 @@ t.equal(quote(['a', { op: '|' }, 'b']), 'a \\| b'); |
-30
| { | ||
| "root": true, | ||
| "extends": "@ljharb", | ||
| "rules": { | ||
| "array-bracket-newline": 0, | ||
| "complexity": 0, | ||
| "eqeqeq": 1, | ||
| "func-style": [2, "declaration"], | ||
| "max-depth": 0, | ||
| "max-lines-per-function": 0, | ||
| "max-statements": 0, | ||
| "multiline-comment-style": 0, | ||
| "no-negated-condition": 1, | ||
| "no-param-reassign": 1, | ||
| "no-lonely-if": 1, | ||
| "no-shadow": 1, | ||
| "no-template-curly-in-string": 0, | ||
| }, | ||
| "overrides": [ | ||
| { | ||
| "files": "example/**", | ||
| "rules": { | ||
| "no-console": 0, | ||
| }, | ||
| }, | ||
| ], | ||
| } |
33477
20.06%21
23.53%766
29.39%13
30%