From e18e5f615ffe91364e2e06fb35c853b914ef57fa Mon Sep 17 00:00:00 2001 From: chen21019 Date: Wed, 7 Oct 2026 01:04:26 +0800 Subject: [PATCH 1/2] release: prepare Web Console 1.6.179 from reviewed main --- README.md | 13 ++++----- ...ass-replacement.node24-ignore-scripts.json | 4 +-- docs/releases/web-console-1.6.179.md | 28 +++++++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- scripts/check-modernization-blockers | 4 +-- scripts/check-ui-console-workspace | 2 +- scripts/check-ui-critical-high-dependencies | 2 +- 8 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 docs/releases/web-console-1.6.179.md diff --git a/README.md b/README.md index 560e601c02..8b457949ac 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,14 @@ history, authorship, licenses, and dependency notices. ## Current release -[Web Console 1.6.178](https://github.com/PastureStack/web-console/releases/tag/1.6.178) -is packaged in [Server v1.6.516](https://github.com/PastureStack/server/releases/tag/v1.6.516). -It fixes inactive-environment view/edit loading: globally authorized project and -member data remains available, while inapplicable network/policy-manager reads -are skipped and explained in all thirteen packaged locales. It does not bypass -active-environment permissions or enable network writes in inactive environments. +[Web Console 1.6.179](https://github.com/PastureStack/web-console/releases/tag/1.6.179) +packages the reviewed Moment 2.31.0 and compatible dependency updates already +merged on `main`. Its archive is published independently of Server assembly. +The prior [Server v1.6.516](https://github.com/PastureStack/server/releases/tag/v1.6.516) +packages Web Console 1.6.178. For component identities, checksums, focused tests, and known verification limits, -see the [current release note](docs/releases/web-console-1.6.178.md). +see the [current release note](docs/releases/web-console-1.6.179.md). Historical changes are in [release notes](docs/releases), not this quick-start guide. Use the Server image for deployment; the console archive alone is not a control plane. diff --git a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json index e4f6fca61f..21b779a849 100644 --- a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json +++ b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.178", + "version": "1.6.179", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.178", + "version": "1.6.179", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/docs/releases/web-console-1.6.179.md b/docs/releases/web-console-1.6.179.md new file mode 100644 index 0000000000..9efb0b7f64 --- /dev/null +++ b/docs/releases/web-console-1.6.179.md @@ -0,0 +1,28 @@ +# Web Console 1.6.179 — reviewed dependency baseline + +This component release packages the dependency updates already merged through +PRs #180 and #182, based on `main` commit +`629e5714984afa9e66671c099773170f54519580`. The reviewed lock resolves Moment +2.31.0, markdown-it 14.3.2, postcss-selector-parser 7.1.6, proxy-addr 2.0.8, +compression 1.8.2 and source-map-js 1.2.2. Prior application behavior, including +inactive-environment details and ended-workspace safeguards, remains unchanged. + +The release diff updates only numeric version metadata, the corresponding +reviewed lock roots, existing gate version constants, README and this note. +It adds no application feature or dependency graph change beyond that merged +baseline. The existing Critical/High audit threshold, fail-closed checks and +dated `GHSA-vfj7-8cjw-p6xm` build-input review remain unchanged; this is not a +zero-CVE claim or runtime not-affected VEX. + +Publication uses the existing fixed-source `Validate Web Console` workflow: +source and supply-chain gates, Chrome unit tests, two production builds and a +byte comparison of the deterministic numeric-root archives. The immutable +[1.6.179 release](https://github.com/PastureStack/web-console/releases/tag/1.6.179) +records the tested source, normal signed PR merge, exact CI run, archive SHA-256 +and size. Its assets reuse the retained CI archive and portable checksum without +rebuilding. The new numeric lightweight tag binds the tested signed commit; +the tag itself is not signed. Previous component tags and assets are preserved. + +Server assembly and deployed browser acceptance are separate results. This +component publication does not claim deployment, complete permission/resource/ +locale coverage, or promotion of any historical HOLD or INCOMPLETE result. diff --git a/package-lock.json b/package-lock.json index e4f6fca61f..21b779a849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.178", + "version": "1.6.179", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.178", + "version": "1.6.179", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index 5ea04469b9..b44b7b6d22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.178", + "version": "1.6.179", "private": true, "description": "PastureStack browser console for the compatible control platform.", "repository": { diff --git a/scripts/check-modernization-blockers b/scripts/check-modernization-blockers index 69984e687a..660da49668 100755 --- a/scripts/check-modernization-blockers +++ b/scripts/check-modernization-blockers @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f: print(json.load(f).get('version', '')) PY ) -if [[ "$version" != "1.6.178" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.178" +if [[ "$version" != "1.6.179" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.179" failures=$((failures + 1)) fi diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index c5eb40c6e2..0388823f48 100755 --- a/scripts/check-ui-console-workspace +++ b/scripts/check-ui-console-workspace @@ -143,4 +143,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI fi printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \ - 1.6.178 browser-session broker-broadcast + 1.6.179 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index 3caafd7317..79bee5949b 100755 --- a/scripts/check-ui-critical-high-dependencies +++ b/scripts/check-ui-critical-high-dependencies @@ -70,7 +70,7 @@ if lock_bytes != baseline_bytes: lock = json.loads(lock_bytes) packages = lock.get("packages", {}) root = packages.get("", {}) -if package.get("version") != "1.6.178": +if package.get("version") != "1.6.179": fail(f"unexpected Web Console version: {package.get('version')}") if root.get("version") != package.get("version"): fail(f"lock root version differs: {root.get('version')}") From 826bff55b8885efc9ff1faec0272ef442e4ff702 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Wed, 7 Oct 2026 01:18:53 +0800 Subject: [PATCH 2/2] security: update all shell-quote copies to official 1.11.0 --- README.md | 3 +- ...ass-replacement.node24-ignore-scripts.json | 8 +- docs/releases/web-console-1.6.179.md | 25 ++++-- package-lock.json | 8 +- package.json | 2 +- scripts/node24-lock-smoke.js | 34 +++++++- tests/unit/utils/shell-quote-test.js | 30 ++++++++ vendor/shell-quote/UPSTREAM.md | 9 ++- vendor/shell-quote/shell-quote-global.js | 77 +++++++++++++++++-- 9 files changed, 167 insertions(+), 29 deletions(-) create mode 100644 tests/unit/utils/shell-quote-test.js diff --git a/README.md b/README.md index 8b457949ac..45eb8c0996 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ history, authorship, licenses, and dependency notices. [Web Console 1.6.179](https://github.com/PastureStack/web-console/releases/tag/1.6.179) packages the reviewed Moment 2.31.0 and compatible dependency updates already -merged on `main`. Its archive is published independently of Server assembly. +merged on `main`, plus the official shell-quote 1.11.0 security fix in both npm +and the browser bundle. Its archive is published independently of Server assembly. The prior [Server v1.6.516](https://github.com/PastureStack/server/releases/tag/v1.6.516) packages Web Console 1.6.178. diff --git a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json index 21b779a849..fcbc54a8f2 100644 --- a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json +++ b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json @@ -75,7 +75,7 @@ "rtlcss": "4.3.0", "semver": "7.8.5", "serialize-javascript": "7.1.0", - "shell-quote": "1.10.0", + "shell-quote": "1.11.0", "socket.io-client": "4.8.3", "sort-package-json": "file:vendor/sort-package-json-compat", "source-map-url": "file:vendor/source-map-url-compat/source-map-url-0.4.0-rc16.0.tgz", @@ -17622,9 +17622,9 @@ } }, "node_modules/shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.11.0.tgz", + "integrity": "sha512-JdxDPD0DBTyu08pq0kPC0xSNet/qsU07qT6IsX1AS8oO2ICNRY4ldNa8OAI6PuwAH8tG3lxEhbqmyp4Dw4036g==", "dev": true, "license": "MIT", "engines": { diff --git a/docs/releases/web-console-1.6.179.md b/docs/releases/web-console-1.6.179.md index 9efb0b7f64..246999ca9a 100644 --- a/docs/releases/web-console-1.6.179.md +++ b/docs/releases/web-console-1.6.179.md @@ -1,16 +1,27 @@ -# Web Console 1.6.179 — reviewed dependency baseline +# Web Console 1.6.179 — reviewed dependencies and shell quoting fix This component release packages the dependency updates already merged through PRs #180 and #182, based on `main` commit `629e5714984afa9e66671c099773170f54519580`. The reviewed lock resolves Moment 2.31.0, markdown-it 14.3.2, postcss-selector-parser 7.1.6, proxy-addr 2.0.8, -compression 1.8.2 and source-map-js 1.2.2. Prior application behavior, including -inactive-environment details and ended-workspace safeguards, remains unchanged. +compression 1.8.2 and source-map-js 1.2.2. It also updates shell-quote from 1.10.0 +to official minimum-fixed 1.11.0 in npm and the vendored browser bundle. -The release diff updates only numeric version metadata, the corresponding -reviewed lock roots, existing gate version constants, README and this note. -It adds no application feature or dependency graph change beyond that merged -baseline. The existing Critical/High audit threshold, fail-closed checks and +The first exact-source CI [37500749166](https://github.com/PastureStack/web-console/actions/runs/37500749166) +failed closed on the newly published Critical +[GHSA-pqg4-j6r4-53mv](https://github.com/advisories/GHSA-pqg4-j6r4-53mv). +The affected library accepted line terminators in a string after a comment +token. The browser bundle contains that library, although the current product +callers do not establish this comment-then-string precondition: input-command +only parses, and the catalog answer preview quotes a single answer token. +This is library remediation, not a confirmed product command-injection claim. + +The release diff updates numeric version metadata, the reviewed lock, +existing gate version constants, shell-quote vendor provenance and smoke pins, +focused security/legitimate-input regressions, README and this note. The browser +wrapper and application callers remain unchanged. The complete official 1.11.0 +module bodies are used; no local security backport or new application feature +is introduced. The existing Critical/High audit threshold, fail-closed checks and dated `GHSA-vfj7-8cjw-p6xm` build-input review remain unchanged; this is not a zero-CVE claim or runtime not-affected VEX. diff --git a/package-lock.json b/package-lock.json index 21b779a849..fcbc54a8f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "rtlcss": "4.3.0", "semver": "7.8.5", "serialize-javascript": "7.1.0", - "shell-quote": "1.10.0", + "shell-quote": "1.11.0", "socket.io-client": "4.8.3", "sort-package-json": "file:vendor/sort-package-json-compat", "source-map-url": "file:vendor/source-map-url-compat/source-map-url-0.4.0-rc16.0.tgz", @@ -17622,9 +17622,9 @@ } }, "node_modules/shell-quote": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", - "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.11.0.tgz", + "integrity": "sha512-JdxDPD0DBTyu08pq0kPC0xSNet/qsU07qT6IsX1AS8oO2ICNRY4ldNa8OAI6PuwAH8tG3lxEhbqmyp4Dw4036g==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index b44b7b6d22..00a9c70bd2 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "rtlcss": "4.3.0", "semver": "7.8.5", "serialize-javascript": "7.1.0", - "shell-quote": "1.10.0", + "shell-quote": "1.11.0", "socket.io-client": "4.8.3", "sort-package-json": "file:vendor/sort-package-json-compat", "source-map-url": "file:vendor/source-map-url-compat/source-map-url-0.4.0-rc16.0.tgz", diff --git a/scripts/node24-lock-smoke.js b/scripts/node24-lock-smoke.js index 0fd641b4b5..8fb1651888 100644 --- a/scripts/node24-lock-smoke.js +++ b/scripts/node24-lock-smoke.js @@ -964,7 +964,7 @@ function expectBrowserifyReplacementVendorGlobals() { const expected = { "vendor/ansi-up/ansi-up-global.js": "a50281fdb1fbe71cf638f09e897d4f5b153a418f731be2db410c796982f75682", "vendor/semver/semver-global.js": "d3c2df6e4e516f21e66e52675f1baf85ba753842fb3f603827f8815ecbc41e9b", - "vendor/shell-quote/shell-quote-global.js": "cdfa04900aae1f1cf27d3c06e6658534eebf74c48edb11dec3b83f9b7dbd4fa8", + "vendor/shell-quote/shell-quote-global.js": "bb1719d929d5435124120975b8df02c9084aea3907688d6125d0a80cf8665b8d", }; const sandbox = { window: {}, self: {}, exports: undefined, module: undefined, define: undefined }; sandbox.global = sandbox; @@ -988,13 +988,41 @@ function expectBrowserifyReplacementVendorGlobals() { if (!shellQuote.quote(["hello world"]).includes("'hello world'")) { fail("vendored shell-quote quote smoke failed"); } + for (const implementation of [shellQuote, require("shell-quote")]) { + for (const terminator of ["\n", "\r", "\u2028", "\u2029"]) { + const hostile = "a" + terminator + "id;#"; + for (const tokens of [ + ["echo", "ok", { comment: "x" }, hostile], + implementation.parse("echo http://example.com/#fragment").concat(hostile), + ]) { + let rejected = false; + try { + implementation.quote(tokens); + } catch (error) { + rejected = error.name === "TypeError"; + } + if (!rejected) { + fail("shell-quote comment line-terminator rejection failed"); + } + } + } + for (const answer of ["", "hello world", "O'Brien!", "$HOME; echo value", "line\nvalue"]) { + if (JSON.stringify(implementation.parse(implementation.quote([answer]))) !== JSON.stringify([answer])) { + fail("shell-quote legitimate single-token roundtrip failed"); + } + } + if (implementation.quote(["echo", { comment: "x" }, "ordinary"]) !== "echo #x ordinary") { + fail("shell-quote ordinary post-comment token changed"); + } + } + console.log("shell-quote-comment-boundary-smoke-ok implementations=2 hostile_cases=16 single_token_controls=10"); const ansiUp = new AnsiUp(); ansiUp.escape_html = false; const ansiHtml = ansiUp.ansi_to_html("\u001b[31mred\u001b[0m <x>"); if (!ansiHtml.includes("red") || ansiHtml.includes("&lt;")) { fail(`vendored ansi_up smoke failed: ${ansiHtml}`); } - console.log("browserify-replacement-vendor-smoke-ok semver=5.7.2 shell-quote=1.10.0 ansi_up=6.0.6"); + console.log("browserify-replacement-vendor-smoke-ok semver=5.7.2 shell-quote=1.11.0 ansi_up=6.0.6"); } function expectCommonmarkBrowserGlobal(file) { @@ -1541,7 +1569,7 @@ expectPackageJsonVersion("md5-jkmyers", "0.0.1"); expectVersion("async", "3.2.6"); expectVersion("prismjs", "1.30.0"); expectVersion("lodash", "4.18.1"); -expectVersion("shell-quote", "1.10.0"); +expectVersion("shell-quote", "1.11.0"); expectVersion("dagre-d3-es", "7.0.14"); expectVersion("commonmark", "0.31.2"); expectPackageJsonVersion("billboard.js", "4.0.3"); diff --git a/tests/unit/utils/shell-quote-test.js b/tests/unit/utils/shell-quote-test.js new file mode 100644 index 0000000000..0951d348f2 --- /dev/null +++ b/tests/unit/utils/shell-quote-test.js @@ -0,0 +1,30 @@ +import { module, test } from 'qunit'; +import ShellQuote from 'ui/utils/shell-quote'; + +module('Unit | Utility | Shell quote', function() { + test('rejects all line terminators after a comment token', function(assert) { + ['\n', '\r', '\u2028', '\u2029'].forEach((terminator) => { + assert.throws(() => ShellQuote.quote([ + 'echo', 'ok', { comment: 'x' }, `a${ terminator }id;#`, + ]), /after a `comment` must not contain line terminators/); + }); + }); + + test('rejects appended hostile strings after a parsed mid-word comment', function(assert) { + const command = ShellQuote.parse('echo http://example.com/#fragment'); + assert.ok(command.some((token) => token && typeof token === 'object' && 'comment' in token)); + ['\n', '\r', '\u2028', '\u2029'].forEach((terminator) => { + assert.throws(() => ShellQuote.quote(command.concat(`a${ terminator }id;#`)), + /after a `comment` must not contain line terminators/); + }); + }); + + test('preserves legitimate command parsing and single-token catalog quoting', function(assert) { + assert.deepEqual(ShellQuote.parse("echo 'hello world'"), ['echo', 'hello world']); + ['', 'hello world', "O'Brien!", '$HOME; echo value', 'line\nvalue'].forEach((answer) => { + assert.deepEqual(ShellQuote.parse(ShellQuote.quote([answer])), [answer]); + }); + assert.strictEqual(ShellQuote.quote(['echo', { comment: 'x' }, 'ordinary']), 'echo #x ordinary'); + assert.strictEqual(ShellQuote.quote(['line\nvalue', { comment: 'x' }]), "'line\nvalue' #x"); + }); +}); diff --git a/vendor/shell-quote/UPSTREAM.md b/vendor/shell-quote/UPSTREAM.md index 819ec7904e..eda449b51d 100644 --- a/vendor/shell-quote/UPSTREAM.md +++ b/vendor/shell-quote/UPSTREAM.md @@ -1,7 +1,14 @@ # Vendored shell-quote Browser Bundle -- Source package: `shell-quote@1.10.0` from the Node 24 no-publish lock baseline. +- Source package: `shell-quote@1.11.0` from the reviewed Node 24 lock baseline. +- Official package integrity: `sha512-JdxDPD0DBTyu08pq0kPC0xSNet/qsU07qT6IsX1AS8oO2ICNRY4ldNa8OAI6PuwAH8tG3lxEhbqmyp4Dw4036g==`. - Source files: `index.js`, `parse.js`, and `quote.js` bundled with browserify standalone name `rc16ShellQuote`. - License: MIT. This preserves the existing PastureStack Web Console shell parse/quote behavior while removing `ember-browserify` and `npm:shell-quote` from the application build path. + +Version 1.11.0 includes the official fix for +[GHSA-pqg4-j6r4-53mv](https://github.com/advisories/GHSA-pqg4-j6r4-53mv): strings +after a comment token cannot contain line terminators. The existing standalone +wrapper is retained; its three module bodies match the integrity-verified +official package. The MIT license text is unchanged. diff --git a/vendor/shell-quote/shell-quote-global.js b/vendor/shell-quote/shell-quote-global.js index ac87f38c3e..792b89f2b1 100644 --- a/vendor/shell-quote/shell-quote-global.js +++ b/vendor/shell-quote/shell-quote-global.js @@ -32,7 +32,12 @@ var CONTROL = /** @type {const} */ ('(?:') + /** @type {const} */ ([ var controlRE = new RegExp('^' + CONTROL + '$'); var META = /** @type {const} */ ('|&;()<> \\t'); var SINGLE_QUOTE = /** @type {const} */ ('\'([^\']*?)\''); -var DOUBLE_QUOTE = /** @type {const} */ ('"((\\\\"|[^"])*?)"'); +// bash ANSI-C quoting, `$'...'`: a backslash escapes the next character, including `'` +var ANSI_C_BODY = '(?:\\\\[\\s\\S]|[^\\\\\'])*?'; +var ANSI_C_QUOTE = '\\$\'' + ANSI_C_BODY + '\''; +var ansiCAt = new RegExp('\\$\'' + ANSI_C_BODY + '(?:(\')|\\\\?$)', 'g'); +var ANSI_C_LETTERS = 'abeEfnrtv'; +var ANSI_C_CHARS = '\x07\b\x1B\x1B\f\n\r\t\v'; var hash = /^#$/; var SQ = /** @type {const} */ ("'"); @@ -87,6 +92,46 @@ function getVar(env, pre, key) { return pre + r; } +var ansiCEscape = /\\([0-7]{1,3}|x[\dA-Fa-f]{1,2}|u[\dA-Fa-f]{1,4}|U[\dA-Fa-f]{1,8}|c(?:\\\\|[\s\S])|[abeEfnrtv\\'"?])/g; + +/** + * @param {string} m + * @param {string} escape + */ +function expandAnsiCEscape(m, escape) { + var kind = escape.charAt(0); + if (kind === 'c') { + var ctrl = escape.charAt(1); + return ctrl === '?' ? '\x7F' : String.fromCharCode(ctrl.charCodeAt(0) & 0x1F); + } + if (kind === 'x' || kind === 'u' || kind === 'U') { + var cp = parseInt(escape.slice(1), 16); + if (cp > 0x10FFFF) { + return m; + } + return String.fromCharCode.apply(null, cp > 0xFFFF ? [0xD7C0 + (cp >> 10), 0xDC00 + (cp & 0x3FF)] : [cp]); + } + if (kind >= '0' && kind <= '7') { + return String.fromCharCode(parseInt(escape, 8) & 0xFF); + } + var letter = ANSI_C_LETTERS.indexOf(escape); + return letter < 0 ? escape : ANSI_C_CHARS.charAt(letter); +} + +/** @param {string} body */ +function expandAnsiC(body) { + return body.replace(ansiCEscape, expandAnsiCEscape).split('\0')[0]; // like bash, a NUL ends the string +} + +/** + * @param {string} s + * @param {number} i + */ +function closesAnsiC(s, i) { + ansiCAt.lastIndex = i; + return !!(/** @type {RegExpExecArray} */ (ansiCAt.exec(s)))[1]; +} + /** * @param {string} string * @param {Env} [env] @@ -99,11 +144,12 @@ function parseInternal(string, env, 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 BAREWORD = '(\\' + BS + '[\'"$\\' + BS + META + ']|\\$\\$|\\$(?!' + ANSI_C_QUOTE.slice(2) + ')|[^\\s\'"$' + META + '])+'; + var DOUBLE_QUOTE = '"(?:\\' + BS + '[\\s\\S]|[^"\\' + BS + '])*"'; var chunker = new RegExp([ '(' + CONTROL + ')', // control chars - '(' + BAREWORD + '|' + DOUBLE_QUOTE + '|' + SINGLE_QUOTE + ')+' + '(' + ANSI_C_QUOTE + '|' + BAREWORD + '|' + DOUBLE_QUOTE + '|' + SINGLE_QUOTE + ')+' ].join('|'), 'g'); var matches = matchAll(string, chunker); @@ -180,9 +226,8 @@ function parseInternal(string, env, opts) { varend -= 1; varname = s.slice(i, varend); i = varend; - } else if ((/[*@#?$!_-]/).test(char)) { + } else if ((/[*@#?$!-]/).test(char)) { varname = char; - i += 1; } else { var slicedFromI = s.slice(i); varend = slicedFromI.match(/[^\w\d_]/); @@ -259,6 +304,11 @@ function parseInternal(string, env, opts) { return /** @type {const} */ ([commentObj]); } else if (c === BS) { esc = true; + } else if (c === DS && s.charAt(i + 1) === SQ && closesAnsiC(s, i)) { + flushRun(); + sawQuote = true; + out += expandAnsiC(s.slice(i + 2, ansiCAt.lastIndex - 1)); + i = ansiCAt.lastIndex - 1; } else if (c === DS) { var value = parseEnvVar(); if (!ifs) { @@ -361,11 +411,15 @@ var OPS = /** @type {const} */ ([ '>' ]); var LINE_TERMINATORS = /[\n\r\u2028\u2029]/; -var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|]/g; +var GLOB_SHELL_SPECIAL = /[\s#!"$&'():;<=>@\\^`|~]/g; /** @type {typeof import('./quote')} */ module.exports = function quote(xs) { + var sawComment = false; return xs.map(function (s) { + if (sawComment && typeof s === 'string' && LINE_TERMINATORS.test(s)) { + throw new TypeError('a token after a `comment` must not contain line terminators'); + } if (s === '') { return /** @type {const} */ ('\'\''); } @@ -377,6 +431,9 @@ module.exports = function quote(xs) { if (LINE_TERMINATORS.test(s.pattern)) { throw new TypeError('glob `pattern` must not contain line terminators'); } + if (s.pattern === '') { + return /** @type {const} */ ('\'\''); + } return s.pattern.replace(GLOB_SHELL_SPECIAL, '\\$&'); } if ('op' in s && typeof s.op === 'string') { @@ -389,15 +446,19 @@ module.exports = function quote(xs) { if (LINE_TERMINATORS.test(s.comment)) { throw new TypeError('`comment` must not contain line terminators'); } + sawComment = true; return '#' + s.comment; } throw new TypeError('unrecognized object token shape'); } + if ((/'/).test(s) && (/!/).test(s)) { + return "'" + s.replace(/'/g, "'\"'\"'") + "'"; + } if ((/["\s\\]/).test(s) && !(/'/).test(s)) { - return "'" + s.replace(/(['])/g, '\\$1') + "'"; + return "'" + s + "'"; } if ((/["'\s]/).test(s)) { - return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"'; + return '"' + s.replace(/(["\\$`])/g, '\\$1') + '"'; } return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}~])/g, '$1\\$2'); }).join(' ');