sh-ast 0.0.0 → 0.1.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 +159 -0
- package/dist/analyze/enumerate-commands.d.ts.map +1 -0
- package/dist/analyze/enumerate-commands.js +390 -0
- package/dist/analyze/enumerate-commands.js.map +1 -0
- package/dist/analyze/index.d.ts +18 -0
- package/dist/analyze/index.d.ts.map +1 -0
- package/dist/analyze/index.js +27 -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-word.d.ts +146 -0
- package/dist/analyze/resolve-word.d.ts.map +1 -0
- package/dist/analyze/resolve-word.js +202 -0
- package/dist/analyze/resolve-word.js.map +1 -0
- package/dist/analyze.d.ts +390 -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 +115 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +106 -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.d.ts +45 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +118 -0
- package/dist/parse.js.map +1 -0
- package/dist/sh-ast.d.ts +1270 -0
- package/dist/types.d.ts +70 -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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ShNode } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* True iff `value` has the minimal shape every normalized {@link ShNode}
|
|
4
|
+
* carries (an object, not an array, with a string `type` field). Used to
|
|
5
|
+
* narrow `unknown` field values discovered while walking a node's own
|
|
6
|
+
* properties — every node field on {@link ShNode} is typed `unknown` (see
|
|
7
|
+
* its index signature), so any code that inspects a specific field's value
|
|
8
|
+
* needs this runtime check before treating it as a child node.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function isShNodeShape(value: unknown): value is ShNode;
|
|
13
|
+
/**
|
|
14
|
+
* Filters `value` down to the {@link ShNode} elements of an array-shaped
|
|
15
|
+
* field, discarding anything else — and returning `[]` for a non-array or
|
|
16
|
+
* `undefined` field, so callers never need a separate `Array.isArray` guard.
|
|
17
|
+
*
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export declare function nodeArray(value: unknown): readonly ShNode[];
|
|
21
|
+
/**
|
|
22
|
+
* Reads `node[field]` as a string, defaulting to `''` for any other shape
|
|
23
|
+
* (missing field, non-string value).
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export declare function stringField(node: ShNode, field: string): string;
|
|
28
|
+
//# sourceMappingURL=node-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-helpers.d.ts","sourceRoot":"","sources":["../../src/analyze/node-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAO7D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAE3D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/D"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True iff `value` has the minimal shape every normalized {@link ShNode}
|
|
3
|
+
* carries (an object, not an array, with a string `type` field). Used to
|
|
4
|
+
* narrow `unknown` field values discovered while walking a node's own
|
|
5
|
+
* properties — every node field on {@link ShNode} is typed `unknown` (see
|
|
6
|
+
* its index signature), so any code that inspects a specific field's value
|
|
7
|
+
* needs this runtime check before treating it as a child node.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export function isShNodeShape(value) {
|
|
12
|
+
return (typeof value === 'object' &&
|
|
13
|
+
value !== null &&
|
|
14
|
+
!Array.isArray(value) &&
|
|
15
|
+
typeof value.type === 'string');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Filters `value` down to the {@link ShNode} elements of an array-shaped
|
|
19
|
+
* field, discarding anything else — and returning `[]` for a non-array or
|
|
20
|
+
* `undefined` field, so callers never need a separate `Array.isArray` guard.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export function nodeArray(value) {
|
|
25
|
+
return Array.isArray(value) ? value.filter(isShNodeShape) : [];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reads `node[field]` as a string, defaulting to `''` for any other shape
|
|
29
|
+
* (missing field, non-string value).
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export function stringField(node, field) {
|
|
34
|
+
const value = node[field];
|
|
35
|
+
return typeof value === 'string' ? value : '';
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=node-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-helpers.js","sourceRoot":"","sources":["../../src/analyze/node-helpers.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACrB,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,CACvD,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,KAAa;IACrD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC1B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAChD,CAAC"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { ShNode } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Why a {@link resolveWord} result is `static: false`: the word's exact
|
|
4
|
+
* text cannot be determined from source alone.
|
|
5
|
+
*
|
|
6
|
+
* - `'expansion'` — the word contains a `ParamExp` (`$x`, `${x}`), a
|
|
7
|
+
* `CmdSubst` (command substitution, either `$(...)` or backtick form), an
|
|
8
|
+
* `ArithmExp` (`$((expr))`), or a `ProcSubst` (`<(cmd)`/`>(cmd)`) part:
|
|
9
|
+
* its value depends on program state (variables, command output, process
|
|
10
|
+
* substitution) that only exists at run time.
|
|
11
|
+
* - `'tilde'` — an unquoted, unescaped `~` triggers Bash tilde expansion
|
|
12
|
+
* (Bash Reference Manual §3.5.2, "Tilde Expansion") at the start of the
|
|
13
|
+
* word, or — when `resolveWord`'s `context` option is `'assignment-value'`
|
|
14
|
+
* or omitted — immediately after an unquoted `:` anywhere in the word too
|
|
15
|
+
* (§3.5.2 also documents this for assignment-statement values, e.g.
|
|
16
|
+
* `PATH=/foo:~/bar`). Either way: statically shaped, but its value
|
|
17
|
+
* depends on the filesystem/environment (`$HOME`, NSS user database
|
|
18
|
+
* lookups) at run time. See `resolveWord`'s `context` parameter doc for
|
|
19
|
+
* the full command-argument-vs-assignment-value distinction.
|
|
20
|
+
* - `'glob'` — the word contains an unquoted, unescaped glob metacharacter
|
|
21
|
+
* (`*`/`?`), an unquoted bracket expression (`[...]` — an unquoted `[`
|
|
22
|
+
* with a later unquoted `]` in the same word, POSIX 2.13.1 / Bash
|
|
23
|
+
* Reference Manual §3.5.8.1, "Pattern Matching"), or an `ExtGlob` part
|
|
24
|
+
* (Bash Reference Manual §3.5.8, "Filename Expansion"): its value depends
|
|
25
|
+
* on which filesystem entries exist at run time.
|
|
26
|
+
* - `'brace'` — the word contains a `BraceExp` part (`{a,b}`/`{1..10}`,
|
|
27
|
+
* Bash Reference Manual §3.5.1, "Brace Expansion"). mvdan/sh v3.13.1 only
|
|
28
|
+
* ever produces this node type when its caller applies `syntax.SplitBraces`
|
|
29
|
+
* to the parsed tree; this bridge's shim does not call it, so today a
|
|
30
|
+
* brace expression like `{a,b}` reaches {@link resolveWord} as an
|
|
31
|
+
* ordinary `Lit` (indistinguishable, in the AST this bridge produces,
|
|
32
|
+
* from literal `{`/`,`/`}` characters) rather than as a `BraceExp` part.
|
|
33
|
+
* This reason is handled here for forward-compatibility with that node
|
|
34
|
+
* type and is not reachable through {@link resolveWord} today.
|
|
35
|
+
* - `'locale'` — the word contains a `$"..."` locale-translated string
|
|
36
|
+
* (Bash Reference Manual §3.1.2.5, "Locale-Specific Translation"): its
|
|
37
|
+
* value is looked up in the current locale's translation catalog at run
|
|
38
|
+
* time via `gettext`, regardless of whether its contents also contain an
|
|
39
|
+
* expansion. `'locale'` takes precedence over any `'expansion'` a
|
|
40
|
+
* `$"..."` word's *contents* would otherwise report — the `DblQuoted`
|
|
41
|
+
* node's own `dollar` flag is checked before its children are visited at
|
|
42
|
+
* all, so e.g. `$"hi $x"` reports `'locale'`, never `'expansion'`.
|
|
43
|
+
* - `'unsupported'` — the word contains a `$'...'` (ANSI-C quoted) `\u`/`\U`
|
|
44
|
+
* escape whose value has no corresponding Unicode character (greater than
|
|
45
|
+
* `U+10FFFF`, e.g. `$'\UFFFFFFFF'`): real bash's output for this case is
|
|
46
|
+
* an undocumented, non-UTF-8 byte sequence with no exact representation
|
|
47
|
+
* as a JavaScript string (see `ansi-c-escapes.ts`'s
|
|
48
|
+
* `MAX_UNICODE_CODE_POINT` doc comment for the empirical detail) — this
|
|
49
|
+
* package declines to claim a `static: true` text bash never actually
|
|
50
|
+
* produces.
|
|
51
|
+
*
|
|
52
|
+
* This union may grow in a **minor** release — new mvdan/sh word-part node
|
|
53
|
+
* types, or new statically-shaped-but-dynamic categories, can add new reason
|
|
54
|
+
* strings without that being a breaking change. It is deliberately not
|
|
55
|
+
* sealed against extension elsewhere in the codebase. A reason string this
|
|
56
|
+
* version of the package doesn't yet know about only ever accompanies
|
|
57
|
+
* `static: false`, never changes the meaning of `static: true`, so runtime
|
|
58
|
+
* consumers that only branch on `.static` are safe by construction across
|
|
59
|
+
* such additions; an exhaustive compile-time `switch` over `.reason` should
|
|
60
|
+
* still include a `default` case to stay forward-compatible.
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
export type WordResolutionReason = 'expansion' | 'tilde' | 'glob' | 'brace' | 'locale' | 'unsupported';
|
|
65
|
+
/**
|
|
66
|
+
* The result of statically resolving a `Word` node's text. Reports facts
|
|
67
|
+
* only — never a safety verdict, never a hardcoded command/wrapper list.
|
|
68
|
+
* `static: false` ("unknowable") is a first-class, valid result, not an
|
|
69
|
+
* error condition.
|
|
70
|
+
*
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
export type WordResolution = {
|
|
74
|
+
/** The word is a compile-time-known string. */
|
|
75
|
+
readonly static: true;
|
|
76
|
+
/** The word's exact resolved text (quotes and escapes removed). */
|
|
77
|
+
readonly text: string;
|
|
78
|
+
} | {
|
|
79
|
+
/** The word's value depends on program state, the environment, or the filesystem. */
|
|
80
|
+
readonly static: false;
|
|
81
|
+
/** Why this word cannot be statically resolved. */
|
|
82
|
+
readonly reason: WordResolutionReason;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Options controlling {@link resolveWord}'s tilde-expansion detection.
|
|
86
|
+
*
|
|
87
|
+
* @public
|
|
88
|
+
*/
|
|
89
|
+
export interface ResolveWordOptions {
|
|
90
|
+
/**
|
|
91
|
+
* Which Bash grammar position `word` occupies — the two positions Bash
|
|
92
|
+
* §3.5.2 ("Tilde Expansion") documents distinct tilde-expansion triggers
|
|
93
|
+
* for:
|
|
94
|
+
*
|
|
95
|
+
* - `'command-argument'` — an ordinary simple-command argument word (e.g.
|
|
96
|
+
* `echo <word>`). Only a *word-initial* unquoted `~` triggers tilde
|
|
97
|
+
* expansion; an unquoted `~` elsewhere in the word (including right
|
|
98
|
+
* after a `:`) is an ordinary character (`echo a:~b` prints `a:~b`
|
|
99
|
+
* literally in real bash).
|
|
100
|
+
* - `'assignment-value'` — the value side of a shell assignment (e.g.
|
|
101
|
+
* `PATH=<word>`, an `Assign` node's `value`). Bash additionally
|
|
102
|
+
* tilde-expands an unquoted `~` immediately following an unquoted `:`
|
|
103
|
+
* anywhere in the value (`PATH=/foo:~/bar` expands the `~/bar`
|
|
104
|
+
* segment) — this is documented for `PATH`-like colon-separated
|
|
105
|
+
* assignment values specifically.
|
|
106
|
+
*
|
|
107
|
+
* **Default: `'assignment-value'` semantics apply whenever this option is
|
|
108
|
+
* omitted.** `resolveWord` takes a bare `Word` node with no grammar
|
|
109
|
+
* context attached, so it cannot see whether its caller extracted that
|
|
110
|
+
* word from a command argument or an assignment value — omitting this
|
|
111
|
+
* option is deliberately treated as the *more conservative* of the two
|
|
112
|
+
* (the one that reports `'tilde'` in more cases), so a caller that
|
|
113
|
+
* doesn't know or care about the distinction never under-reports a real
|
|
114
|
+
* tilde-expansion site. Pass `'command-argument'` explicitly to opt into
|
|
115
|
+
* the narrower, word-initial-only check that matches real bash's
|
|
116
|
+
* behavior for ordinary arguments.
|
|
117
|
+
*
|
|
118
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Tilde-Expansion
|
|
119
|
+
*/
|
|
120
|
+
readonly context?: 'command-argument' | 'assignment-value';
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Determines whether a `Word` node is statically a known string, and if
|
|
124
|
+
* so, what that string is — the atom of shell-AST static analysis (e.g.
|
|
125
|
+
* "is this word literally `rm`"). A word is static iff every part is a
|
|
126
|
+
* literal after shell unquoting: a bare `Lit` (backslash escapes
|
|
127
|
+
* processed), a `SglQuoted` (including `$'...'` ANSI-C escapes decoded),
|
|
128
|
+
* or a `DblQuoted` containing only literal parts (and not itself a
|
|
129
|
+
* `$"..."` locale translation) — including concatenations of these (`'r'm`
|
|
130
|
+
* resolves to `"rm"`). A word containing any expansion (`ParamExp`,
|
|
131
|
+
* `CmdSubst`, `ArithmExp`, `ProcSubst`), a triggering unquoted `~` (see
|
|
132
|
+
* `options.context`), an unquoted glob metacharacter/bracket
|
|
133
|
+
* expression/`ExtGlob`, a `$"..."` locale translation, or an unrepresentable
|
|
134
|
+
* `$'\U...'` escape is `static: false` with a reason — never an error, and
|
|
135
|
+
* never a safety judgment (see {@link WordResolution}).
|
|
136
|
+
*
|
|
137
|
+
* @param word - A `Word` node, e.g. `parseSync(...).stmts[0].cmd.args[0]`.
|
|
138
|
+
* @param options - See {@link ResolveWordOptions}.
|
|
139
|
+
* @throws TypeError if `word.type` is not `"Word"` — a programmer-error
|
|
140
|
+
* misuse of the API, not a "malformed shell source" case (well-formed
|
|
141
|
+
* shell source, however dynamic, never throws; see {@link WordResolution}).
|
|
142
|
+
*
|
|
143
|
+
* @public
|
|
144
|
+
*/
|
|
145
|
+
export declare function resolveWord(word: ShNode, options?: ResolveWordOptions): WordResolution;
|
|
146
|
+
//# sourceMappingURL=resolve-word.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-word.d.ts","sourceRoot":"","sources":["../../src/analyze/resolve-word.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAK1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,MAAM,MAAM,oBAAoB,GAC9B,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEtE;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GACtB;IACE,+CAA+C;IAC/C,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,GACD;IACE,qFAAqF;IACrF,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IACvB,mDAAmD;IACnD,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC,CAAC;AAEN;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,kBAAkB,CAAC;CAC5D;AA6JD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,cAAc,CA4BtF"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { decodeAnsiCString } from './ansi-c-escapes.js';
|
|
2
|
+
import { decodeDblQuotedLit, decodeUnquotedLit } from './decode-lit.js';
|
|
3
|
+
import { nodeArray, stringField } from './node-helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* True iff `raw` (a `Lit` part's *undecoded* value) begins with a literal
|
|
6
|
+
* `~` character. Only meaningful when checked against a `Word`'s very
|
|
7
|
+
* first part: Bash tilde expansion (§3.5.2) triggers only at the start of
|
|
8
|
+
* a word, so `a~b` (mid-word) and `\~foo` (escaped — `raw` starts with a
|
|
9
|
+
* backslash, not `~`) are correctly excluded, while `~"foo"` (a leading
|
|
10
|
+
* unquoted `~` followed by a separately-quoted part) is correctly
|
|
11
|
+
* included, since it is still a distinct first `Lit` part starting with an
|
|
12
|
+
* unquoted `~`.
|
|
13
|
+
*/
|
|
14
|
+
function startsWithUnescapedTilde(raw) {
|
|
15
|
+
return raw.startsWith('~');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* True iff an unquoted, unescaped `~` immediately follows an unquoted,
|
|
19
|
+
* unescaped `:` anywhere across `parts` — Bash §3.5.2's assignment-value
|
|
20
|
+
* tilde-expansion trigger (e.g. the `~/bar` in `PATH=/foo:~/bar`).
|
|
21
|
+
*
|
|
22
|
+
* Only a `Word`'s top-level `Lit` parts carry unquoted text; any other
|
|
23
|
+
* part type (`SglQuoted`, `DblQuoted`, `ParamExp`, …) breaks adjacency —
|
|
24
|
+
* whatever text or expansion it represents sits between the `:` and any
|
|
25
|
+
* following `~`, so they are no longer *immediately* adjacent. `:`/`~`
|
|
26
|
+
* adjacency is tracked across concatenated `Lit` parts (state carried
|
|
27
|
+
* between loop iterations), since concatenation joins parts with no
|
|
28
|
+
* grammar-level separator between them.
|
|
29
|
+
*/
|
|
30
|
+
function hasUnquotedColonTilde(parts) {
|
|
31
|
+
let prevWasUnescapedColon = false;
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
if (part.type !== 'Lit') {
|
|
34
|
+
prevWasUnescapedColon = false;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const raw = stringField(part, 'value');
|
|
38
|
+
let i = 0;
|
|
39
|
+
while (i < raw.length) {
|
|
40
|
+
const ch = raw[i];
|
|
41
|
+
if (ch === '\\' && i + 1 < raw.length) {
|
|
42
|
+
// An escaped character (whatever it is) is never an unescaped `:`
|
|
43
|
+
// or `~`, and breaks adjacency for the purposes of this scan.
|
|
44
|
+
prevWasUnescapedColon = false;
|
|
45
|
+
i += 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (prevWasUnescapedColon && ch === '~') {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
prevWasUnescapedColon = ch === ':';
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* True iff `parts` contains an unquoted bracket expression: an unescaped
|
|
59
|
+
* `[` with a later unescaped `]` anywhere in the same word (POSIX 2.13.1 /
|
|
60
|
+
* Bash Reference Manual §3.5.8.1, "Pattern Matching"). `a[bc` (no closing
|
|
61
|
+
* bracket) is a *literal* filename in Bash; `a[bc]d` is a pattern. The
|
|
62
|
+
* closing `]` may live in a different, concatenated `Lit` part than the
|
|
63
|
+
* opening `[` (`decodeUnquotedLit`'s `brackets` output is concatenated,
|
|
64
|
+
* in part order, across every top-level `Lit` part first) — and content
|
|
65
|
+
* *between* the two, even if it came from an unquoted or quoted part, does
|
|
66
|
+
* not prevent them from forming a bracket expression (`a["b"]` matches
|
|
67
|
+
* real bash's file `ab`, verified empirically: quoting `"b"` doesn't
|
|
68
|
+
* suppress the surrounding unquoted `[`/`]`). Only `Lit` parts contribute
|
|
69
|
+
* to the scan — `SglQuoted`/`DblQuoted` text is fully quoted and never
|
|
70
|
+
* glob-significant (`a'['bc']'d`'s brackets are both quoted, so it stays
|
|
71
|
+
* literal).
|
|
72
|
+
*/
|
|
73
|
+
function hasUnquotedBracketExpression(parts) {
|
|
74
|
+
let brackets = '';
|
|
75
|
+
for (const part of parts) {
|
|
76
|
+
if (part.type !== 'Lit')
|
|
77
|
+
continue;
|
|
78
|
+
brackets += decodeUnquotedLit(stringField(part, 'value')).brackets;
|
|
79
|
+
}
|
|
80
|
+
const openIndex = brackets.indexOf('[');
|
|
81
|
+
if (openIndex === -1)
|
|
82
|
+
return false;
|
|
83
|
+
return brackets.includes(']', openIndex + 1);
|
|
84
|
+
}
|
|
85
|
+
function resolvePart(part, quoting) {
|
|
86
|
+
switch (part.type) {
|
|
87
|
+
case 'Lit': {
|
|
88
|
+
const raw = stringField(part, 'value');
|
|
89
|
+
if (quoting === 'dblquoted') {
|
|
90
|
+
return { ok: true, text: decodeDblQuotedLit(raw) };
|
|
91
|
+
}
|
|
92
|
+
const { text, hasGlob } = decodeUnquotedLit(raw);
|
|
93
|
+
if (hasGlob)
|
|
94
|
+
return { ok: false, reason: 'glob' };
|
|
95
|
+
return { ok: true, text };
|
|
96
|
+
}
|
|
97
|
+
case 'SglQuoted': {
|
|
98
|
+
const raw = stringField(part, 'value');
|
|
99
|
+
// ANSI-C quoting ($'...') is flagged by SglQuoted.Dollar (mvdan/sh
|
|
100
|
+
// v3.13.1's `syntax.SglQuoted.Dollar bool`); plain single-quoted
|
|
101
|
+
// text ('...') never has any escapes interpreted at all.
|
|
102
|
+
if (part.dollar === true) {
|
|
103
|
+
const decoded = decodeAnsiCString(raw);
|
|
104
|
+
if (!decoded.ok)
|
|
105
|
+
return { ok: false, reason: 'unsupported' };
|
|
106
|
+
return { ok: true, text: decoded.text };
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, text: raw };
|
|
109
|
+
}
|
|
110
|
+
case 'DblQuoted': {
|
|
111
|
+
// Locale translation ($"...") is flagged by DblQuoted.Dollar
|
|
112
|
+
// (mvdan/sh v3.13.1's `syntax.DblQuoted.Dollar bool`) and is checked
|
|
113
|
+
// before descending into this node's children: the whole construct
|
|
114
|
+
// is locale-dependent regardless of its contents (Bash Reference
|
|
115
|
+
// Manual §3.1.2.5), so 'locale' deterministically takes precedence
|
|
116
|
+
// over any 'expansion' (or other reason) its contents would
|
|
117
|
+
// otherwise report — see WordResolutionReason's 'locale' doc.
|
|
118
|
+
if (part.dollar === true) {
|
|
119
|
+
return { ok: false, reason: 'locale' };
|
|
120
|
+
}
|
|
121
|
+
let text = '';
|
|
122
|
+
for (const inner of nodeArray(part.parts)) {
|
|
123
|
+
const result = resolvePart(inner, 'dblquoted');
|
|
124
|
+
if (!result.ok)
|
|
125
|
+
return result;
|
|
126
|
+
text += result.text;
|
|
127
|
+
}
|
|
128
|
+
return { ok: true, text };
|
|
129
|
+
}
|
|
130
|
+
case 'ParamExp':
|
|
131
|
+
case 'CmdSubst':
|
|
132
|
+
case 'ArithmExp':
|
|
133
|
+
case 'ProcSubst':
|
|
134
|
+
return { ok: false, reason: 'expansion' };
|
|
135
|
+
case 'ExtGlob':
|
|
136
|
+
return { ok: false, reason: 'glob' };
|
|
137
|
+
case 'BraceExp':
|
|
138
|
+
// See WordResolutionReason's 'brace' doc: unreachable via this
|
|
139
|
+
// bridge's parser today, handled for forward-compatibility.
|
|
140
|
+
return { ok: false, reason: 'brace' };
|
|
141
|
+
default:
|
|
142
|
+
// Any word-part node type this module doesn't specifically know
|
|
143
|
+
// about. "Unknowable" must stay a first-class result for well-formed
|
|
144
|
+
// input rather than a thrown error — 'expansion' is the closest
|
|
145
|
+
// neutral bucket (some run-time-dependent mechanism) until the type
|
|
146
|
+
// is specifically handled.
|
|
147
|
+
return { ok: false, reason: 'expansion' };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Determines whether a `Word` node is statically a known string, and if
|
|
152
|
+
* so, what that string is — the atom of shell-AST static analysis (e.g.
|
|
153
|
+
* "is this word literally `rm`"). A word is static iff every part is a
|
|
154
|
+
* literal after shell unquoting: a bare `Lit` (backslash escapes
|
|
155
|
+
* processed), a `SglQuoted` (including `$'...'` ANSI-C escapes decoded),
|
|
156
|
+
* or a `DblQuoted` containing only literal parts (and not itself a
|
|
157
|
+
* `$"..."` locale translation) — including concatenations of these (`'r'm`
|
|
158
|
+
* resolves to `"rm"`). A word containing any expansion (`ParamExp`,
|
|
159
|
+
* `CmdSubst`, `ArithmExp`, `ProcSubst`), a triggering unquoted `~` (see
|
|
160
|
+
* `options.context`), an unquoted glob metacharacter/bracket
|
|
161
|
+
* expression/`ExtGlob`, a `$"..."` locale translation, or an unrepresentable
|
|
162
|
+
* `$'\U...'` escape is `static: false` with a reason — never an error, and
|
|
163
|
+
* never a safety judgment (see {@link WordResolution}).
|
|
164
|
+
*
|
|
165
|
+
* @param word - A `Word` node, e.g. `parseSync(...).stmts[0].cmd.args[0]`.
|
|
166
|
+
* @param options - See {@link ResolveWordOptions}.
|
|
167
|
+
* @throws TypeError if `word.type` is not `"Word"` — a programmer-error
|
|
168
|
+
* misuse of the API, not a "malformed shell source" case (well-formed
|
|
169
|
+
* shell source, however dynamic, never throws; see {@link WordResolution}).
|
|
170
|
+
*
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
173
|
+
export function resolveWord(word, options) {
|
|
174
|
+
if (word.type !== 'Word') {
|
|
175
|
+
throw new TypeError(`resolveWord expects a "Word" node, got "${word.type}"`);
|
|
176
|
+
}
|
|
177
|
+
const parts = nodeArray(word.parts);
|
|
178
|
+
if (parts.length === 0) {
|
|
179
|
+
return { static: true, text: '' };
|
|
180
|
+
}
|
|
181
|
+
// `parts.length === 0` returned above, so `parts[0]` is a real element.
|
|
182
|
+
const first = parts[0];
|
|
183
|
+
if (first.type === 'Lit' && startsWithUnescapedTilde(stringField(first, 'value'))) {
|
|
184
|
+
return { static: false, reason: 'tilde' };
|
|
185
|
+
}
|
|
186
|
+
if (options?.context !== 'command-argument' && hasUnquotedColonTilde(parts)) {
|
|
187
|
+
return { static: false, reason: 'tilde' };
|
|
188
|
+
}
|
|
189
|
+
if (hasUnquotedBracketExpression(parts)) {
|
|
190
|
+
return { static: false, reason: 'glob' };
|
|
191
|
+
}
|
|
192
|
+
let text = '';
|
|
193
|
+
for (const part of parts) {
|
|
194
|
+
const result = resolvePart(part, 'unquoted');
|
|
195
|
+
if (!result.ok) {
|
|
196
|
+
return { static: false, reason: result.reason };
|
|
197
|
+
}
|
|
198
|
+
text += result.text;
|
|
199
|
+
}
|
|
200
|
+
return { static: true, text };
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=resolve-word.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-word.js","sourceRoot":"","sources":["../../src/analyze/resolve-word.ts"],"names":[],"mappings":"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"}
|