sh-ast 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -0
- package/dist/analyze/ansi-c-escapes.d.ts +53 -0
- package/dist/analyze/ansi-c-escapes.d.ts.map +1 -0
- package/dist/analyze/ansi-c-escapes.js +255 -0
- package/dist/analyze/ansi-c-escapes.js.map +1 -0
- package/dist/analyze/decode-lit.d.ts +74 -0
- package/dist/analyze/decode-lit.d.ts.map +1 -0
- package/dist/analyze/decode-lit.js +114 -0
- package/dist/analyze/decode-lit.js.map +1 -0
- package/dist/analyze/enumerate-commands.d.ts +165 -0
- package/dist/analyze/enumerate-commands.d.ts.map +1 -0
- package/dist/analyze/enumerate-commands.js +396 -0
- package/dist/analyze/enumerate-commands.js.map +1 -0
- package/dist/analyze/index.d.ts +26 -0
- package/dist/analyze/index.d.ts.map +1 -0
- package/dist/analyze/index.js +30 -0
- package/dist/analyze/index.js.map +1 -0
- package/dist/analyze/node-helpers.d.ts +28 -0
- package/dist/analyze/node-helpers.d.ts.map +1 -0
- package/dist/analyze/node-helpers.js +37 -0
- package/dist/analyze/node-helpers.js.map +1 -0
- package/dist/analyze/resolve-argv0.d.ts +335 -0
- package/dist/analyze/resolve-argv0.d.ts.map +1 -0
- package/dist/analyze/resolve-argv0.js +510 -0
- package/dist/analyze/resolve-argv0.js.map +1 -0
- package/dist/analyze/resolve-word.d.ts +151 -0
- package/dist/analyze/resolve-word.d.ts.map +1 -0
- package/dist/analyze/resolve-word.js +207 -0
- package/dist/analyze/resolve-word.js.map +1 -0
- package/dist/analyze.d.ts +771 -0
- package/dist/deep-freeze.d.ts +14 -0
- package/dist/deep-freeze.d.ts.map +1 -0
- package/dist/deep-freeze.js +25 -0
- package/dist/deep-freeze.js.map +1 -0
- package/dist/errors.d.ts +180 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +179 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/normalize.d.ts +52 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +260 -0
- package/dist/normalize.js.map +1 -0
- package/dist/parse-depth-guard.d.ts +11 -0
- package/dist/parse-depth-guard.d.ts.map +1 -0
- package/dist/parse-depth-guard.js +633 -0
- package/dist/parse-depth-guard.js.map +1 -0
- package/dist/parse.d.ts +50 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +125 -0
- package/dist/parse.js.map +1 -0
- package/dist/sh-ast.d.ts +1346 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/visitor-keys.d.ts +21 -0
- package/dist/visitor-keys.d.ts.map +1 -0
- package/dist/visitor-keys.js +23 -0
- package/dist/visitor-keys.js.map +1 -0
- package/dist/walk.d.ts +18 -0
- package/dist/walk.d.ts.map +1 -0
- package/dist/walk.js +40 -0
- package/dist/walk.js.map +1 -0
- package/dist/wasm-instance.d.ts +13 -0
- package/dist/wasm-instance.d.ts.map +1 -0
- package/dist/wasm-instance.js +88 -0
- package/dist/wasm-instance.js.map +1 -0
- package/generated/child-type-schema.d.ts +14 -0
- package/generated/child-type-schema.js +188 -0
- package/generated/node-types.d.ts +958 -0
- package/generated/position-fields.d.ts +21 -0
- package/generated/position-fields.js +50 -0
- package/generated/visitor-keys.d.ts +11 -0
- package/generated/visitor-keys.js +50 -0
- package/package.json +93 -10
- package/shim/sh-ast.wasm +0 -0
- package/shim/wasm_exec.js +654 -0
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# sh-ast
|
|
2
|
+
|
|
3
|
+
A synchronous, fully-typed shell AST for JavaScript and TypeScript. `sh-ast` parses shell scripts
|
|
4
|
+
(bash, POSIX sh, mksh, zsh, and bats) using [mvdan/sh](https://github.com/mvdan/sh) — the Go
|
|
5
|
+
shell parser used by `shfmt` and `shellcheck`-adjacent tooling — via a small Go/WASM shim, and
|
|
6
|
+
normalizes the result into a plain-object AST with UTF-16-safe positions that's natural to
|
|
7
|
+
consume from JS/TS (linters, formatters, codemods, static analysis, or anything else that needs
|
|
8
|
+
to understand shell source structurally).
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { parseSync } from 'sh-ast';
|
|
12
|
+
|
|
13
|
+
const file = parseSync('echo hi');
|
|
14
|
+
file.stmts[0].cmd.type; // "CallExpr"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Provenance
|
|
18
|
+
|
|
19
|
+
`sh-ast` was extracted from [`eslint-sh`](https://github.com/mike-north/eslint-sh) (as
|
|
20
|
+
`@eslint-sh/bridge`) at commit
|
|
21
|
+
[`bdd55b3`](https://github.com/mike-north/eslint-sh/commit/bdd55b3), where it was built as the
|
|
22
|
+
parser core underneath an ESLint plugin for shell scripts. The full commit history predating the
|
|
23
|
+
extraction lives in that repository. This package carries no ESLint dependency and exposes the
|
|
24
|
+
same public API `@eslint-sh/bridge` did.
|
|
25
|
+
|
|
26
|
+
## Status
|
|
27
|
+
|
|
28
|
+
The visitor-keys and child-type-schema tables, plus the `ShNodes.*` TypeScript typings, are
|
|
29
|
+
generated from mvdan/sh's `syntax` package struct definitions by `tools/gen-visitor-keys` — see
|
|
30
|
+
that tool's README for "The schema table is generated, not hand-written".
|
|
31
|
+
|
|
32
|
+
## Public API
|
|
33
|
+
|
|
34
|
+
- `parseSync(text, options?)` — synchronous parse; throws `ShParseError` (with `.line`,
|
|
35
|
+
`.column`, `.filename`) on a shell syntax error.
|
|
36
|
+
- `ParseOptions` — `{ dialect?, filename? }`; `dialect` is one of `bash | posix | mksh | bats |
|
|
37
|
+
zsh`, mapped to mvdan/sh's `LangVariant` by string name.
|
|
38
|
+
- `ShNode` / `ShFile` — normalized node shape (`type`, `range`, `loc`, plus lowercased fields
|
|
39
|
+
from mvdan/sh's tree).
|
|
40
|
+
- `ShNodes` — generated, strongly-typed `ShNode` subtypes for every mvdan/sh node type (e.g.
|
|
41
|
+
`ShNodes.ShCallExprNode`, `ShNodes.ShDeclClauseNode`), plus union aliases (`ShAnyNode`,
|
|
42
|
+
`ShCommandNode`, `ShWordPartNode`, …). Additive and optional — `ShNode`'s index signature
|
|
43
|
+
remains the base contract.
|
|
44
|
+
- `visitorKeys` — generated node-type → child-field-name table.
|
|
45
|
+
- `walk(node, visit)` — visits every node in a normalized tree; discovers children structurally
|
|
46
|
+
rather than depending on `visitorKeys`.
|
|
47
|
+
|
|
48
|
+
## Regenerating the schema tables
|
|
49
|
+
|
|
50
|
+
After bumping the pinned mvdan/sh version (`shim/go.mod` _and_ `../../tools/gen-visitor-keys/go.mod`
|
|
51
|
+
must move together):
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
pnpm run generate:sh-ast-schema
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This regenerates `generated/visitor-keys.js(.d.ts)`, `generated/child-type-schema.js(.d.ts)`, and
|
|
58
|
+
`generated/node-types.d.ts`, then formats them with Prettier. CI
|
|
59
|
+
(`.github/workflows/gen-visitor-keys.yml`) reruns the same script and fails the build if the
|
|
60
|
+
committed artifacts don't come back byte-identical — the drift gate for future mvdan/sh bumps.
|
|
61
|
+
Re-run the kitchen-sink golden test (`test/kitchen-sink.test.ts`) afterward; it fails if the new
|
|
62
|
+
mvdan/sh version adds a node type the fixtures don't yet exercise.
|
|
63
|
+
|
|
64
|
+
## The WASM shim
|
|
65
|
+
|
|
66
|
+
`shim/main.go` parses with `mvdan.cc/sh/v3/syntax` and encodes the result with
|
|
67
|
+
`shim/internal/nodeencode` — a performance fork of `mvdan.cc/sh/v3/syntax/typedjson`'s encode
|
|
68
|
+
path (see that package's doc comment for why: typedjson's reflection-based encoder rebuilds an
|
|
69
|
+
identical synthetic struct type per node _instance_ rather than once per node _type_, which
|
|
70
|
+
dominated warm-parse latency — see "Performance" below) — so interface-typed nodes (`CallExpr`,
|
|
71
|
+
`BinaryCmd`, …) carry a `Type` discriminator, exactly as upstream typedjson does (verified
|
|
72
|
+
byte-identical in `shim/internal/nodeencode/encode_test.go`). The whole result envelope —
|
|
73
|
+
including any error message — is marshaled with Go's `encoding/json`, so JSON-significant bytes
|
|
74
|
+
in an error message (e.g. an unclosed-quote message containing `"`) survive the Go→JS boundary
|
|
75
|
+
intact.
|
|
76
|
+
|
|
77
|
+
The shim exposes a linear-memory ABI — `alloc`/`process`/`free` WASM exports plus the `mem`
|
|
78
|
+
memory export — instead of a `syscall/js` global function. `wasm-instance.ts` writes UTF-8 bytes
|
|
79
|
+
directly into WASM memory and reads the JSON result back with `TextDecoder`, avoiding
|
|
80
|
+
`syscall/js`'s much more expensive per-call JS↔Go value marshaling (see "Performance" below).
|
|
81
|
+
|
|
82
|
+
### Performance
|
|
83
|
+
|
|
84
|
+
Local measurement (700-line/~9k-node fixture, warm instance, Apple M-series):
|
|
85
|
+
|
|
86
|
+
| Stage | Before (syscall/js + typedjson) | After (linear memory + nodeencode) |
|
|
87
|
+
| ------------------------------------- | ------------------------------: | ---------------------------------: |
|
|
88
|
+
| End-to-end warm `parseSync` (p50) | ~97 ms | ~29 ms |
|
|
89
|
+
| wasm call (parse + encode + transfer) | ~91 ms | ~23.5 ms |
|
|
90
|
+
| `JSON.parse` | ~3 ms | ~3 ms |
|
|
91
|
+
| `normalize` | ~2.5 ms | ~2.7 ms |
|
|
92
|
+
| `sh-ast.wasm` size | ~4.10 MB | ~3.82 MB |
|
|
93
|
+
|
|
94
|
+
The dominant fix was **not** the transport switch alone — profiling found `typedjson.Encode`'s
|
|
95
|
+
`reflect.StructOf` call (rebuilding an identical synthetic struct type once per node _instance_)
|
|
96
|
+
responsible for ~80ms of the ~90ms wasm call; `nodeencode`'s per-_type_ cache cut that to ~14ms.
|
|
97
|
+
The linear-memory ABI switch and a hand-rolled envelope wrap (skipping a redundant
|
|
98
|
+
`encoding/json.Marshal` re-compaction of the already-encoded AST bytes) account for the rest.
|
|
99
|
+
|
|
100
|
+
**TinyGo**, evaluated for its ~4x smaller runtime, cannot run this shim: it compiles
|
|
101
|
+
`reflect.StructOf` calls without error but panics at runtime with `unimplemented:
|
|
102
|
+
reflect.StructOf()` the moment `nodeencode`/`typedjson`'s encoder runs (confirmed against
|
|
103
|
+
TinyGo 0.41.1). Standard Go + `wasm-opt -Oz` (binaryen, pinned in CI) is the fallback:
|
|
104
|
+
`sh-ast.wasm` is optimized after every build, for a ~7% size reduction (~4.09 MB → ~3.82 MB) with
|
|
105
|
+
no behavior change. Neither path reaches a 1.5 MB stretch target.
|
|
106
|
+
|
|
107
|
+
After editing `shim/main.go` or `shim/internal/nodeencode`, rebuild the committed
|
|
108
|
+
`shim/sh-ast.wasm`:
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
cd shim
|
|
112
|
+
GOOS=js GOARCH=wasm go build -buildvcs=false -trimpath -ldflags="-s -w -buildid=" -o sh-ast.wasm .
|
|
113
|
+
wasm-opt -Oz --enable-bulk-memory-opt --enable-nontrapping-float-to-int sh-ast.wasm -o sh-ast.wasm
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`wasm-opt` ships with [binaryen](https://github.com/WebAssembly/binaryen/releases) — CI installs
|
|
117
|
+
a pinned release (see `.github/workflows/wasm-reproducibility.yml`'s `BINARYEN_VERSION`); install
|
|
118
|
+
the same version locally (`brew install binaryen`, or download the pinned release directly) so
|
|
119
|
+
your rebuild matches CI byte-for-byte. `--enable-bulk-memory-opt` and
|
|
120
|
+
`--enable-nontrapping-float-to-int` tell `wasm-opt` to accept the two WASM proposal features
|
|
121
|
+
Go's `js/wasm` target already emits (both broadly supported in Node and browsers since ~2020) —
|
|
122
|
+
without them, `wasm-opt` rejects the input as invalid.
|
|
123
|
+
|
|
124
|
+
The `-buildid=` flag blanks out Go's build-ID metadata (used only by the Go toolchain's own
|
|
125
|
+
build cache, never read at runtime); without it, `-trimpath` alone is not enough for a
|
|
126
|
+
byte-identical rebuild — the embedded build ID otherwise varies between build-cache states even
|
|
127
|
+
with identical source and dependencies.
|
|
128
|
+
|
|
129
|
+
`-buildvcs=false` is equally required: Go 1.18+ stamps binaries with `vcs.revision`/`vcs.time`/
|
|
130
|
+
`vcs.modified` by default (`-buildvcs=auto`), reading whatever commit and dirty-state the
|
|
131
|
+
checkout happens to be in at build time. That stamp isn't touched by `-s -w` (it lives outside
|
|
132
|
+
the symbol table/DWARF that those flags strip), so two builds of _identical source_ differ
|
|
133
|
+
whenever the checkout's commit or dirty-state differs — which is exactly what happens for a
|
|
134
|
+
`pull_request`-triggered CI run (GitHub synthesizes a fresh merge commit, with its own SHA and
|
|
135
|
+
timestamp, on every run) versus a `push`-triggered run (checks out the named branch commit
|
|
136
|
+
as-is), and equally for a local checkout with any uncommitted changes elsewhere in the working
|
|
137
|
+
tree. `-buildvcs=false` removes this source of non-determinism entirely; verified byte-for-byte
|
|
138
|
+
identical across a native macOS/arm64 build, a `linux/amd64` build under Docker/QEMU emulation,
|
|
139
|
+
and real `ubuntu-latest` CI runs triggered via `push`, `pull_request`, and `workflow_dispatch`.
|
|
140
|
+
|
|
141
|
+
`wasm-opt -Oz` is a deterministic, version-pinned transform: rebuilding twice (Go build +
|
|
142
|
+
wasm-opt) produces byte-identical output, verified locally and by CI's `rebuild-match` job.
|
|
143
|
+
|
|
144
|
+
CI rebuilds the shim with a pinned Go toolchain (`shim/go.mod`) and a pinned `wasm-opt`
|
|
145
|
+
(`.github/workflows/wasm-reproducibility.yml`), and fails if the output differs from the
|
|
146
|
+
committed artifact. It also runs `shim/internal/nodeencode`'s Go test suite, which asserts the
|
|
147
|
+
fork's output is byte-identical to upstream `typedjson.Encode`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes `$'...'` (Bash ANSI-C Quoting) escape sequences.
|
|
3
|
+
*
|
|
4
|
+
* mvdan/sh's parser does **not** perform this decoding: for a `SglQuoted`
|
|
5
|
+
* word part with `dollar: true`, the normalized `value` field is the raw,
|
|
6
|
+
* un-decoded source text between the quotes (verified empirically against
|
|
7
|
+
* mvdan.cc/sh/v3 v3.13.1 — see `test/analyze-resolve-word.test.ts`'s module
|
|
8
|
+
* comment for how this was confirmed). This module fills that gap using the
|
|
9
|
+
* escape table the Bash Reference Manual documents, independently of
|
|
10
|
+
* whatever mvdan/sh itself does or doesn't decode.
|
|
11
|
+
*
|
|
12
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The result of {@link decodeAnsiCString}: either the fully decoded text,
|
|
17
|
+
* or `ok: false` when the raw text contains a `\u`/`\U` escape whose value
|
|
18
|
+
* has no corresponding Unicode character (see {@link MAX_UNICODE_CODE_POINT}'s
|
|
19
|
+
* doc comment for why that case can't be decoded to an exact JavaScript
|
|
20
|
+
* string, and can't be left as literal un-decoded text either without
|
|
21
|
+
* claiming a false `static: true` result). Never throws — callers (see
|
|
22
|
+
* `resolveWord`'s `SglQuoted` handling) turn `ok: false` into
|
|
23
|
+
* `{ static: false, reason: 'unsupported' }` for the whole word.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export type AnsiCDecodeResult = {
|
|
28
|
+
readonly ok: true;
|
|
29
|
+
readonly text: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly ok: false;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Decodes the raw inner text of a `$'...'` (ANSI-C quoted) string per Bash
|
|
35
|
+
* Reference Manual §3.1.2.4, "ANSI-C Quoting". `raw` is the text between
|
|
36
|
+
* the quotes (mvdan/sh's `SglQuoted.Value` for a `Dollar: true` node),
|
|
37
|
+
* exactly as it appears in source — not yet interpreted in any way.
|
|
38
|
+
*
|
|
39
|
+
* Recognized escapes: `\a \b \e \E \f \n \r \t \v \\ \' \" \?`, octal
|
|
40
|
+
* `\nnn` (one to three digits), hex `\xHH` (one or two digits), Unicode
|
|
41
|
+
* `\uHHHH` (one to four digits) and `\UHHHHHHHH` (one to eight digits), and
|
|
42
|
+
* control-character `\cX`. An unrecognized backslash sequence (not in this
|
|
43
|
+
* table, and not a recognized numeric escape) is left as-is — Bash's own
|
|
44
|
+
* decoding is best-effort for malformed input, and this function mirrors
|
|
45
|
+
* that rather than throwing, matching {@link resolveWord}'s "never throw
|
|
46
|
+
* for well-formed input" contract at the string level too. A `\u`/`\U`
|
|
47
|
+
* whose value *is* well-formed but has no corresponding Unicode character
|
|
48
|
+
* is a different case — see {@link AnsiCDecodeResult}.
|
|
49
|
+
*
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
export declare function decodeAnsiCString(raw: string): AnsiCDecodeResult;
|
|
53
|
+
//# sourceMappingURL=ansi-c-escapes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ansi-c-escapes.d.ts","sourceRoot":"","sources":["../../src/analyze/ansi-c-escapes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAwFH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,iBAAiB,GAC3B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;CAAE,CAAC;AAExE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CA8IhE"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes `$'...'` (Bash ANSI-C Quoting) escape sequences.
|
|
3
|
+
*
|
|
4
|
+
* mvdan/sh's parser does **not** perform this decoding: for a `SglQuoted`
|
|
5
|
+
* word part with `dollar: true`, the normalized `value` field is the raw,
|
|
6
|
+
* un-decoded source text between the quotes (verified empirically against
|
|
7
|
+
* mvdan.cc/sh/v3 v3.13.1 — see `test/analyze-resolve-word.test.ts`'s module
|
|
8
|
+
* comment for how this was confirmed). This module fills that gap using the
|
|
9
|
+
* escape table the Bash Reference Manual documents, independently of
|
|
10
|
+
* whatever mvdan/sh itself does or doesn't decode.
|
|
11
|
+
*
|
|
12
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Hex digit predicate for `\xHH`/`\uHHHH`/`\UHHHHHHHH`. `ch` is expected to
|
|
17
|
+
* come from `String.prototype.charAt`, which returns `''` (never
|
|
18
|
+
* `undefined`) past the end of the string — `''` correctly fails this test,
|
|
19
|
+
* so callers don't need a separate bounds check.
|
|
20
|
+
*/
|
|
21
|
+
function isHexDigit(ch) {
|
|
22
|
+
return /^[0-9a-fA-F]$/.test(ch);
|
|
23
|
+
}
|
|
24
|
+
/** Octal digit predicate for `\nnn`; see {@link isHexDigit}'s `charAt` note. */
|
|
25
|
+
function isOctalDigit(ch) {
|
|
26
|
+
return ch.length === 1 && ch >= '0' && ch <= '7';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The highest Unicode scalar value `\uHHHH`/`\UHHHHHHHH` can validly decode
|
|
30
|
+
* to. `\uHHHH` (max 4 hex digits, i.e. `0xFFFF`) can never exceed this, but
|
|
31
|
+
* `\UHHHHHHHH` (up to 8 hex digits, i.e. up to `0xFFFFFFFF`) routinely does.
|
|
32
|
+
*
|
|
33
|
+
* The Bash Reference Manual's ANSI-C Quoting table documents `\UHHHHHHHH`
|
|
34
|
+
* only as "the Unicode ... character whose value is the hexadecimal value
|
|
35
|
+
* HHHHHHHH" — it does not say what happens when that value has no
|
|
36
|
+
* corresponding Unicode character (i.e. is greater than `U+10FFFF`, the
|
|
37
|
+
* highest code point Unicode defines). Real bash's behavior for this case
|
|
38
|
+
* is undocumented and not a reasonable decode target: it falls back to a
|
|
39
|
+
* pre-RFC-3629, up-to-6-byte "UTF-8-like" bit-packing with no range
|
|
40
|
+
* validation (confirmed empirically — e.g. `$'\U00110000'` produces the
|
|
41
|
+
* raw byte sequence `f4 90 80 80`, which is not valid UTF-8 text, and
|
|
42
|
+
* `$'\UFFFFFFFF'` produces no output at all, exit 0). Neither of those is
|
|
43
|
+
* representable as an exact JavaScript string, so this module does **not**
|
|
44
|
+
* fall back to treating the escape as literal, un-decoded text the way it
|
|
45
|
+
* does for other unrecognized escapes — that would silently claim a
|
|
46
|
+
* `static: true` exact text that real bash never produces (a false
|
|
47
|
+
* static). Instead, {@link decodeAnsiCString} reports an out-of-range
|
|
48
|
+
* `\u`/`\U` as *unrepresentable* via {@link AnsiCDecodeResult}, and
|
|
49
|
+
* `resolveWord` propagates that as `{ static: false, reason: 'unsupported' }`
|
|
50
|
+
* for the whole word — never `String.fromCodePoint`, which throws
|
|
51
|
+
* `RangeError` for any value over this limit and would violate
|
|
52
|
+
* `resolveWord`'s "never throw for well-formed input" contract.
|
|
53
|
+
*
|
|
54
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#ANSI_002dC-Quoting
|
|
55
|
+
*/
|
|
56
|
+
const MAX_UNICODE_CODE_POINT = 0x10ffff;
|
|
57
|
+
/**
|
|
58
|
+
* Greedily consumes up to `maxDigits` hex digits starting at `start`,
|
|
59
|
+
* returning the parsed numeric value and how many digits were consumed (`0`
|
|
60
|
+
* if none were valid).
|
|
61
|
+
*/
|
|
62
|
+
function readHexDigits(source, start, maxDigits) {
|
|
63
|
+
let digits = '';
|
|
64
|
+
let index = start;
|
|
65
|
+
while (digits.length < maxDigits) {
|
|
66
|
+
const ch = source.charAt(index);
|
|
67
|
+
if (!isHexDigit(ch))
|
|
68
|
+
break;
|
|
69
|
+
digits += ch;
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
return { value: digits.length > 0 ? parseInt(digits, 16) : 0, length: digits.length };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Greedily consumes up to `maxDigits` octal digits starting at `start`,
|
|
76
|
+
* returning the parsed numeric value and how many digits were consumed.
|
|
77
|
+
*/
|
|
78
|
+
function readOctalDigits(source, start, maxDigits) {
|
|
79
|
+
let digits = '';
|
|
80
|
+
let index = start;
|
|
81
|
+
while (digits.length < maxDigits) {
|
|
82
|
+
const ch = source.charAt(index);
|
|
83
|
+
if (!isOctalDigit(ch))
|
|
84
|
+
break;
|
|
85
|
+
digits += ch;
|
|
86
|
+
index += 1;
|
|
87
|
+
}
|
|
88
|
+
return { value: digits.length > 0 ? parseInt(digits, 8) : 0, length: digits.length };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Decodes the raw inner text of a `$'...'` (ANSI-C quoted) string per Bash
|
|
92
|
+
* Reference Manual §3.1.2.4, "ANSI-C Quoting". `raw` is the text between
|
|
93
|
+
* the quotes (mvdan/sh's `SglQuoted.Value` for a `Dollar: true` node),
|
|
94
|
+
* exactly as it appears in source — not yet interpreted in any way.
|
|
95
|
+
*
|
|
96
|
+
* Recognized escapes: `\a \b \e \E \f \n \r \t \v \\ \' \" \?`, octal
|
|
97
|
+
* `\nnn` (one to three digits), hex `\xHH` (one or two digits), Unicode
|
|
98
|
+
* `\uHHHH` (one to four digits) and `\UHHHHHHHH` (one to eight digits), and
|
|
99
|
+
* control-character `\cX`. An unrecognized backslash sequence (not in this
|
|
100
|
+
* table, and not a recognized numeric escape) is left as-is — Bash's own
|
|
101
|
+
* decoding is best-effort for malformed input, and this function mirrors
|
|
102
|
+
* that rather than throwing, matching {@link resolveWord}'s "never throw
|
|
103
|
+
* for well-formed input" contract at the string level too. A `\u`/`\U`
|
|
104
|
+
* whose value *is* well-formed but has no corresponding Unicode character
|
|
105
|
+
* is a different case — see {@link AnsiCDecodeResult}.
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
export function decodeAnsiCString(raw) {
|
|
110
|
+
let text = '';
|
|
111
|
+
let i = 0;
|
|
112
|
+
while (i < raw.length) {
|
|
113
|
+
const ch = raw[i];
|
|
114
|
+
if (ch !== '\\' || i + 1 >= raw.length) {
|
|
115
|
+
// `i < raw.length` (the while guard) means indexing here is always
|
|
116
|
+
// in-bounds and `ch` is a real character, not `undefined`.
|
|
117
|
+
text += ch;
|
|
118
|
+
i += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
// `i + 1 < raw.length` was just confirmed above, so this index is safe.
|
|
122
|
+
const next = raw[i + 1];
|
|
123
|
+
switch (next) {
|
|
124
|
+
case 'a':
|
|
125
|
+
text += '\x07';
|
|
126
|
+
i += 2;
|
|
127
|
+
break;
|
|
128
|
+
case 'b':
|
|
129
|
+
text += '\x08';
|
|
130
|
+
i += 2;
|
|
131
|
+
break;
|
|
132
|
+
case 'e':
|
|
133
|
+
case 'E':
|
|
134
|
+
text += '\x1b';
|
|
135
|
+
i += 2;
|
|
136
|
+
break;
|
|
137
|
+
case 'f':
|
|
138
|
+
text += '\x0c';
|
|
139
|
+
i += 2;
|
|
140
|
+
break;
|
|
141
|
+
case 'n':
|
|
142
|
+
text += '\n';
|
|
143
|
+
i += 2;
|
|
144
|
+
break;
|
|
145
|
+
case 'r':
|
|
146
|
+
text += '\r';
|
|
147
|
+
i += 2;
|
|
148
|
+
break;
|
|
149
|
+
case 't':
|
|
150
|
+
text += '\t';
|
|
151
|
+
i += 2;
|
|
152
|
+
break;
|
|
153
|
+
case 'v':
|
|
154
|
+
text += '\x0b';
|
|
155
|
+
i += 2;
|
|
156
|
+
break;
|
|
157
|
+
case '\\':
|
|
158
|
+
text += '\\';
|
|
159
|
+
i += 2;
|
|
160
|
+
break;
|
|
161
|
+
case "'":
|
|
162
|
+
text += "'";
|
|
163
|
+
i += 2;
|
|
164
|
+
break;
|
|
165
|
+
case '"':
|
|
166
|
+
text += '"';
|
|
167
|
+
i += 2;
|
|
168
|
+
break;
|
|
169
|
+
case '?':
|
|
170
|
+
text += '?';
|
|
171
|
+
i += 2;
|
|
172
|
+
break;
|
|
173
|
+
case 'x': {
|
|
174
|
+
const { value, length } = readHexDigits(raw, i + 2, 2);
|
|
175
|
+
if (length > 0) {
|
|
176
|
+
text += String.fromCharCode(value);
|
|
177
|
+
i += 2 + length;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
text += ch;
|
|
181
|
+
i += 1;
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'u': {
|
|
186
|
+
const { value, length } = readHexDigits(raw, i + 2, 4);
|
|
187
|
+
if (length === 0) {
|
|
188
|
+
text += ch;
|
|
189
|
+
i += 1;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// `\uHHHH` allows at most 4 hex digits (max 0xFFFF), which can
|
|
193
|
+
// never exceed MAX_UNICODE_CODE_POINT (0x10FFFF) — this check is
|
|
194
|
+
// purely defensive, mirroring the `\U` case below, and is
|
|
195
|
+
// unreachable today (see MAX_UNICODE_CODE_POINT's doc comment).
|
|
196
|
+
if (value > MAX_UNICODE_CODE_POINT) {
|
|
197
|
+
return { ok: false };
|
|
198
|
+
}
|
|
199
|
+
text += String.fromCodePoint(value);
|
|
200
|
+
i += 2 + length;
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'U': {
|
|
204
|
+
const { value, length } = readHexDigits(raw, i + 2, 8);
|
|
205
|
+
if (length === 0) {
|
|
206
|
+
// Not a recognized numeric escape (no valid hex digit at all):
|
|
207
|
+
// leave un-decoded, same as any other unrecognized escape.
|
|
208
|
+
text += ch;
|
|
209
|
+
i += 1;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (value > MAX_UNICODE_CODE_POINT) {
|
|
213
|
+
// Well-formed 8-digit hex value, but out of Unicode's range (see
|
|
214
|
+
// MAX_UNICODE_CODE_POINT's doc comment): bash's real output for
|
|
215
|
+
// this case can't be represented as an exact JS string, so this
|
|
216
|
+
// is not the same as an "unrecognized escape" — signal failure
|
|
217
|
+
// for the whole string rather than claiming a false literal.
|
|
218
|
+
return { ok: false };
|
|
219
|
+
}
|
|
220
|
+
text += String.fromCodePoint(value);
|
|
221
|
+
i += 2 + length;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case 'c': {
|
|
225
|
+
const controlChar = raw.charAt(i + 2);
|
|
226
|
+
if (controlChar !== '') {
|
|
227
|
+
// Bash's control-character formula: the char XOR 0x40 (e.g. 'A'
|
|
228
|
+
// 0x41 ^ 0x40 = 0x01). See ANSI-C Quoting's "\cx" entry.
|
|
229
|
+
const code = (controlChar.toUpperCase().charCodeAt(0) ^ 0x40) & 0xff;
|
|
230
|
+
text += String.fromCharCode(code);
|
|
231
|
+
i += 3;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
text += ch;
|
|
235
|
+
i += 1;
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
default:
|
|
240
|
+
if (isOctalDigit(next)) {
|
|
241
|
+
const { value, length } = readOctalDigits(raw, i + 1, 3);
|
|
242
|
+
text += String.fromCharCode(value);
|
|
243
|
+
i += 1 + length;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Not a recognized escape: leave the backslash (and whatever
|
|
247
|
+
// follows) untouched, one character at a time.
|
|
248
|
+
text += ch;
|
|
249
|
+
i += 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { ok: true, text };
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=ansi-c-escapes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ansi-c-escapes.js","sourceRoot":"","sources":["../../src/analyze/ansi-c-escapes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes `Lit` word-part text and detects unquoted glob metacharacters.
|
|
3
|
+
* mvdan/sh's `Lit.Value` is raw source text for ordinary backslash escapes
|
|
4
|
+
* — a non-quoted backslash and the character it escapes both remain in
|
|
5
|
+
* `Lit.Value` exactly as written, undecoded (verified empirically; see
|
|
6
|
+
* `test/analyze-resolve-word.test.ts`) — so this module performs the two
|
|
7
|
+
* context-dependent decodings Bash itself defines for a `Lit` depending on
|
|
8
|
+
* whether it is a direct child of a `Word` ("unquoted") or nested inside a
|
|
9
|
+
* `DblQuoted` ("double-quoted"). The one exception is a backslash-newline
|
|
10
|
+
* *line continuation*: mvdan/sh's lexer consumes and discards it before
|
|
11
|
+
* `Lit.Value` is ever populated (verified empirically — parsing
|
|
12
|
+
* `cmd a\` + newline + `s` yields a `Lit` whose `value` is `"as"`, with no
|
|
13
|
+
* trace of the backslash or newline), so unlike ordinary escapes it never
|
|
14
|
+
* reaches this module and there is nothing here left to decode for it.
|
|
15
|
+
*
|
|
16
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Escape-Character
|
|
17
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Double-Quotes
|
|
18
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Pattern-Matching
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The result of decoding a `Lit` part that is a direct (unquoted) child of
|
|
23
|
+
* a `Word`.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export interface UnquotedLitResult {
|
|
28
|
+
/** The decoded literal text (escapes resolved, glob chars included verbatim). */
|
|
29
|
+
readonly text: string;
|
|
30
|
+
/** `true` iff an unescaped `*` or `?` appears in `raw`. */
|
|
31
|
+
readonly hasGlob: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Every unescaped `[` or `]` in `raw`, concatenated in source order (e.g.
|
|
34
|
+
* `raw`'s unescaped occurrences in `"a[b]c]"` yield `"[]]"`). A caller
|
|
35
|
+
* assembling a `Word`'s full bracket-expression classification
|
|
36
|
+
* concatenates this across all of a word's top-level `Lit` parts, in
|
|
37
|
+
* part order, then checks for a `[` with a later `]` — see
|
|
38
|
+
* `resolveWord`'s bracket scan and POSIX 2.13.1 / Bash §3.5.8.1.
|
|
39
|
+
*/
|
|
40
|
+
readonly brackets: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Decodes a `Lit` part's raw value when it is an unquoted, direct child of
|
|
44
|
+
* a `Word` (not nested inside `DblQuoted`). Per Bash Reference Manual
|
|
45
|
+
* §3.1.2.1, "Escape Character": a non-quoted backslash preserves the
|
|
46
|
+
* literal value of the very next character. (A backslash-newline line
|
|
47
|
+
* continuation never reaches this function at all — see this module's
|
|
48
|
+
* doc comment — so there is no such case to handle here.) Escaping a glob
|
|
49
|
+
* metacharacter or bracket character suppresses its special meaning, so
|
|
50
|
+
* `hasGlob` and `brackets` only ever reflect *unescaped* occurrences.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export declare function decodeUnquotedLit(raw: string): UnquotedLitResult;
|
|
55
|
+
/**
|
|
56
|
+
* Decodes a `Lit` part's raw value when nested inside a `DblQuoted`. Per
|
|
57
|
+
* Bash Reference Manual §3.1.2.3, "Double Quotes": inside double quotes, a
|
|
58
|
+
* backslash retains its special meaning only when followed by `$`, `` ` ``,
|
|
59
|
+
* `"`, `\`, or newline; any other backslash is preserved literally (both
|
|
60
|
+
* the backslash and the following character remain in the result). (As
|
|
61
|
+
* with {@link decodeUnquotedLit}, a backslash-newline line continuation
|
|
62
|
+
* never reaches this function — mvdan/sh's lexer strips it before
|
|
63
|
+
* `Lit.Value` is populated even inside double quotes, verified empirically:
|
|
64
|
+
* parsing `"a\` + newline + `b"` yields two sibling `Lit` nodes, `"a"` and
|
|
65
|
+
* `"b"`, with no `Lit` ever containing the backslash or newline — so there
|
|
66
|
+
* is no such case for this function to decode.) Quoting always suppresses
|
|
67
|
+
* pattern matching (Filename Expansion never applies to quoted text), so
|
|
68
|
+
* double-quoted `Lit` text never contributes to a word's glob
|
|
69
|
+
* classification — this function has no `hasGlob` output.
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export declare function decodeDblQuotedLit(raw: string): string;
|
|
74
|
+
//# sourceMappingURL=decode-lit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode-lit.d.ts","sourceRoot":"","sources":["../../src/analyze/decode-lit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAmBH;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAuBhE;AAKD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAqBtD"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decodes `Lit` word-part text and detects unquoted glob metacharacters.
|
|
3
|
+
* mvdan/sh's `Lit.Value` is raw source text for ordinary backslash escapes
|
|
4
|
+
* — a non-quoted backslash and the character it escapes both remain in
|
|
5
|
+
* `Lit.Value` exactly as written, undecoded (verified empirically; see
|
|
6
|
+
* `test/analyze-resolve-word.test.ts`) — so this module performs the two
|
|
7
|
+
* context-dependent decodings Bash itself defines for a `Lit` depending on
|
|
8
|
+
* whether it is a direct child of a `Word` ("unquoted") or nested inside a
|
|
9
|
+
* `DblQuoted` ("double-quoted"). The one exception is a backslash-newline
|
|
10
|
+
* *line continuation*: mvdan/sh's lexer consumes and discards it before
|
|
11
|
+
* `Lit.Value` is ever populated (verified empirically — parsing
|
|
12
|
+
* `cmd a\` + newline + `s` yields a `Lit` whose `value` is `"as"`, with no
|
|
13
|
+
* trace of the backslash or newline), so unlike ordinary escapes it never
|
|
14
|
+
* reaches this module and there is nothing here left to decode for it.
|
|
15
|
+
*
|
|
16
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Escape-Character
|
|
17
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Double-Quotes
|
|
18
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Pattern-Matching
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Unquoted glob metacharacters this module detects directly, by a single
|
|
23
|
+
* unescaped occurrence: `*` (any string) and `?` (any single character) —
|
|
24
|
+
* Bash Reference Manual §3.5.8.1, "Pattern Matching". Bracket expressions
|
|
25
|
+
* (`[...]`) are handled separately by {@link decodeUnquotedLit}'s
|
|
26
|
+
* `brackets` output, since — per POSIX 2.13.1 and Bash §3.5.8.1 — a lone
|
|
27
|
+
* unquoted `[` is only glob-significant when a later unquoted `]` closes
|
|
28
|
+
* it (`a[bc` with no closing bracket is a *literal* filename in Bash,
|
|
29
|
+
* while `a[bc]` is a pattern), and that closing `]` may live in a
|
|
30
|
+
* different, concatenated `Lit` part of the same `Word` (see
|
|
31
|
+
* `resolveWord`'s cross-part bracket scan).
|
|
32
|
+
*/
|
|
33
|
+
const GLOB_METACHARACTERS = new Set(['*', '?']);
|
|
34
|
+
/** The two characters {@link decodeUnquotedLit} tracks for cross-part bracket-expression detection. */
|
|
35
|
+
const BRACKET_CHARACTERS = new Set(['[', ']']);
|
|
36
|
+
/**
|
|
37
|
+
* Decodes a `Lit` part's raw value when it is an unquoted, direct child of
|
|
38
|
+
* a `Word` (not nested inside `DblQuoted`). Per Bash Reference Manual
|
|
39
|
+
* §3.1.2.1, "Escape Character": a non-quoted backslash preserves the
|
|
40
|
+
* literal value of the very next character. (A backslash-newline line
|
|
41
|
+
* continuation never reaches this function at all — see this module's
|
|
42
|
+
* doc comment — so there is no such case to handle here.) Escaping a glob
|
|
43
|
+
* metacharacter or bracket character suppresses its special meaning, so
|
|
44
|
+
* `hasGlob` and `brackets` only ever reflect *unescaped* occurrences.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function decodeUnquotedLit(raw) {
|
|
49
|
+
let text = '';
|
|
50
|
+
let hasGlob = false;
|
|
51
|
+
let brackets = '';
|
|
52
|
+
let i = 0;
|
|
53
|
+
while (i < raw.length) {
|
|
54
|
+
const ch = raw[i];
|
|
55
|
+
if (ch === '\\' && i + 1 < raw.length) {
|
|
56
|
+
const next = raw[i + 1];
|
|
57
|
+
text += next;
|
|
58
|
+
i += 2;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (GLOB_METACHARACTERS.has(ch)) {
|
|
62
|
+
hasGlob = true;
|
|
63
|
+
}
|
|
64
|
+
if (BRACKET_CHARACTERS.has(ch)) {
|
|
65
|
+
brackets += ch;
|
|
66
|
+
}
|
|
67
|
+
text += ch;
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
return { text, hasGlob, brackets };
|
|
71
|
+
}
|
|
72
|
+
/** Characters after which a backslash retains special meaning inside double quotes. */
|
|
73
|
+
const DBLQUOTED_ESCAPABLE = new Set(['$', '`', '"', '\\']);
|
|
74
|
+
/**
|
|
75
|
+
* Decodes a `Lit` part's raw value when nested inside a `DblQuoted`. Per
|
|
76
|
+
* Bash Reference Manual §3.1.2.3, "Double Quotes": inside double quotes, a
|
|
77
|
+
* backslash retains its special meaning only when followed by `$`, `` ` ``,
|
|
78
|
+
* `"`, `\`, or newline; any other backslash is preserved literally (both
|
|
79
|
+
* the backslash and the following character remain in the result). (As
|
|
80
|
+
* with {@link decodeUnquotedLit}, a backslash-newline line continuation
|
|
81
|
+
* never reaches this function — mvdan/sh's lexer strips it before
|
|
82
|
+
* `Lit.Value` is populated even inside double quotes, verified empirically:
|
|
83
|
+
* parsing `"a\` + newline + `b"` yields two sibling `Lit` nodes, `"a"` and
|
|
84
|
+
* `"b"`, with no `Lit` ever containing the backslash or newline — so there
|
|
85
|
+
* is no such case for this function to decode.) Quoting always suppresses
|
|
86
|
+
* pattern matching (Filename Expansion never applies to quoted text), so
|
|
87
|
+
* double-quoted `Lit` text never contributes to a word's glob
|
|
88
|
+
* classification — this function has no `hasGlob` output.
|
|
89
|
+
*
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
export function decodeDblQuotedLit(raw) {
|
|
93
|
+
let text = '';
|
|
94
|
+
let i = 0;
|
|
95
|
+
while (i < raw.length) {
|
|
96
|
+
const ch = raw[i];
|
|
97
|
+
if (ch === '\\' && i + 1 < raw.length) {
|
|
98
|
+
const next = raw[i + 1];
|
|
99
|
+
if (DBLQUOTED_ESCAPABLE.has(next)) {
|
|
100
|
+
text += next;
|
|
101
|
+
i += 2;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Not one of the escapable characters: the backslash stays literal.
|
|
105
|
+
text += ch;
|
|
106
|
+
i += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
text += ch;
|
|
110
|
+
i += 1;
|
|
111
|
+
}
|
|
112
|
+
return text;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=decode-lit.js.map
|