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
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The result of statically resolving one position in an
|
|
3
|
+
* {@link Argv0Resolution.chain}: either an ordinary {@link WordResolution}
|
|
4
|
+
* (from `resolveWord`) or an {@link Argv0UnresolvedWord} (a `resolveArgv0`-
|
|
5
|
+
* level "we don't know", distinct from any reason `resolveWord` itself
|
|
6
|
+
* would report).
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export declare type Argv0ChainWord = WordResolution | Argv0UnresolvedWord;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The result of following a {@link CommandSite}'s argv0 through zero or
|
|
14
|
+
* more transparent wrappers to the effective command actually invoked.
|
|
15
|
+
* Facts only — no safety verdict; see {@link resolveArgv0}'s doc comment.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export declare interface Argv0Resolution {
|
|
20
|
+
/**
|
|
21
|
+
* Every word resolved along the way, outermost wrapper first, ending
|
|
22
|
+
* with {@link Argv0Resolution.effective} — e.g. for
|
|
23
|
+
* `nohup env A=1 command rm x` this is the four resolutions for
|
|
24
|
+
* `nohup`, `env`, `command`, `rm`, in that order. Always has at least
|
|
25
|
+
* one element.
|
|
26
|
+
*/
|
|
27
|
+
readonly chain: readonly Argv0ChainWord[];
|
|
28
|
+
/**
|
|
29
|
+
* The last element of {@link Argv0Resolution.chain} — the effective
|
|
30
|
+
* command, or, if resolution hit a statically-unknowable word anywhere
|
|
31
|
+
* along the chain (an expansion, a glob, an unrecognized flag, an
|
|
32
|
+
* embedded-command operand, …), that unknowable result itself.
|
|
33
|
+
* **A `static: false` word is never guessed through**: the moment one is
|
|
34
|
+
* reached (whether at argv0, several wrappers deep, or synthesized by
|
|
35
|
+
* `resolveArgv0` itself for an unrecognized flag), it becomes
|
|
36
|
+
* `effective` and the chain stops — this is the security-relevant
|
|
37
|
+
* guarantee that makes `sudo -u x "$prog"` report an unknowable
|
|
38
|
+
* effective command rather than silently treating `sudo` itself as the
|
|
39
|
+
* answer, and makes `sudo -D /tmp rm x` (an unrecognized `sudo` flag)
|
|
40
|
+
* report `'unknown-flag'` rather than misreporting `rm` — or, worse,
|
|
41
|
+
* `/tmp` — as the effective command.
|
|
42
|
+
*/
|
|
43
|
+
readonly effective: Argv0ChainWord;
|
|
44
|
+
/**
|
|
45
|
+
* The number of `VAR=val` shell-assignment prefixes on the `CallExpr`
|
|
46
|
+
* itself (`CommandSite.node.assigns`, e.g. the `FOO=bar` in
|
|
47
|
+
* `FOO=bar rm x`) that were skipped to reach argv0. This is a distinct
|
|
48
|
+
* mechanism from a wrapper's own `VAR=val` *operands* (e.g. `env`'s or
|
|
49
|
+
* `sudo`'s, see {@link WrapperSpec.skipAssignmentOperands}) — always
|
|
50
|
+
* exactly `CommandSite.node.assigns`'s length, independent of the
|
|
51
|
+
* wrapper chain found afterward.
|
|
52
|
+
*/
|
|
53
|
+
readonly assignmentsSkipped: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Why {@link resolveArgv0} couldn't identify a single, definite effective
|
|
58
|
+
* command — distinct from {@link WordResolutionReason}, which is about why
|
|
59
|
+
* one *word*'s text is unknowable at the shell-syntax level (an expansion,
|
|
60
|
+
* a glob, …). `Argv0UnresolvedReason` instead reflects `resolveArgv0`'s own
|
|
61
|
+
* wrapper-table-driven analysis of an otherwise statically-known word:
|
|
62
|
+
*
|
|
63
|
+
* - `'unknown-flag'` — a statically known word shaped like a flag (starts
|
|
64
|
+
* with `-`, isn't exactly `--`) for the wrapper currently being
|
|
65
|
+
* followed, but that doesn't match any flag/operand shape
|
|
66
|
+
* {@link WrapperSpec} recognizes for it. Never guessed through as the
|
|
67
|
+
* wrapped command — see {@link resolveArgv0}'s doc comment.
|
|
68
|
+
* - `'embedded-command'` — a wrapper flag whose value structurally embeds
|
|
69
|
+
* the real command rather than naming it as a separate word (e.g.
|
|
70
|
+
* `env -S 'rm -rf /'` — see {@link WrapperSpec.unresolvableFlags}'s doc
|
|
71
|
+
* comment).
|
|
72
|
+
*
|
|
73
|
+
* This union may grow in a **minor** release (mirroring
|
|
74
|
+
* `WordResolutionReason`'s and `CommandContext`'s semver policy in
|
|
75
|
+
* `resolve-word.ts`/`enumerate-commands.ts`) — a reason this version
|
|
76
|
+
* doesn't yet know about only ever accompanies `static: false`; an
|
|
77
|
+
* exhaustive `switch` should still include a `default` case.
|
|
78
|
+
*
|
|
79
|
+
* @public
|
|
80
|
+
*/
|
|
81
|
+
export declare type Argv0UnresolvedReason = 'unknown-flag' | 'embedded-command';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A word position {@link resolveArgv0} could not resolve to either a known
|
|
85
|
+
* static text or one of {@link WordResolution}'s own `static: false`
|
|
86
|
+
* reasons — see {@link Argv0UnresolvedReason}'s doc comment. Shares
|
|
87
|
+
* `WordResolution`'s `static: false` discriminant shape by design, so a
|
|
88
|
+
* consumer that only branches on `.static` treats it identically to any
|
|
89
|
+
* other unresolvable word.
|
|
90
|
+
*
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
export declare interface Argv0UnresolvedWord {
|
|
94
|
+
readonly static: false;
|
|
95
|
+
readonly reason: Argv0UnresolvedReason;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A single frame of the path from the root of the tree down to a
|
|
100
|
+
* {@link CommandSite}, describing *how* the command is reached — never
|
|
101
|
+
* whether it is safe to run. `CommandSite.context` is an ordered stack of
|
|
102
|
+
* these, outermost frame first:
|
|
103
|
+
*
|
|
104
|
+
* - `'and'`/`'or'` (`side: 'right'`) — the right-hand operand of a
|
|
105
|
+
* `BinaryCmd` (mvdan/sh's node for both `&&` and `||`); only the right
|
|
106
|
+
* side is tagged; the left side inherits the surrounding context
|
|
107
|
+
* unchanged, since it runs unconditionally relative to this operator.
|
|
108
|
+
* - `'pipeline'` (`stage: n`) — one stage of a `|`/`|&` chain (also a
|
|
109
|
+
* `BinaryCmd`, left-associatively nested by mvdan/sh); every stage is
|
|
110
|
+
* tagged, 0-indexed left to right, regardless of whether the chain mixes
|
|
111
|
+
* `|` and `|&`.
|
|
112
|
+
* - `'subshell'` — inside a `Subshell` (`( ... )`).
|
|
113
|
+
* - `'cmdSubst'`/`'procSubst'` — inside a `CmdSubst` (`$(...)`/backticks) or
|
|
114
|
+
* `ProcSubst` (`<(...)`/`>(...)`) reached from *any* word-bearing
|
|
115
|
+
* position (an argument, a redirection target, a case subject, a loop's
|
|
116
|
+
* word list, an assignment value, a test/arithmetic operand, …) — not
|
|
117
|
+
* only from `CallExpr.args`.
|
|
118
|
+
* - `'if'` (`branch: 'cond' | 'then' | 'else'`) — inside an `IfClause`'s
|
|
119
|
+
* condition, then-branch, or else-branch (mvdan/sh nests `elif` chains as
|
|
120
|
+
* `IfClause.else` pointing to another `IfClause`, so an `elif`'s own
|
|
121
|
+
* condition/then are reached through an `{kind:'if',branch:'else'}` frame
|
|
122
|
+
* first, then their own `'cond'`/`'then'` frame — reflecting the real
|
|
123
|
+
* nesting rather than collapsing it).
|
|
124
|
+
* - `'case'` — inside one `CaseClause` branch's statement list (not the
|
|
125
|
+
* case subject word or the patterns).
|
|
126
|
+
* - `'loop'` (`role` is `'body'` or `'cond'`) — inside a `ForClause`'s
|
|
127
|
+
* statement list (`role: 'body'` only — a `for` loop has no
|
|
128
|
+
* statement-list condition) or a `WhileClause`'s (`role: 'cond'` for the
|
|
129
|
+
* condition, `role: 'body'` for the loop body).
|
|
130
|
+
* - `'function'` (`name`) — inside a `FuncDecl`'s body; `name` is the
|
|
131
|
+
* function's literal name text.
|
|
132
|
+
* - `'background'`/`'negated'` — the enclosing `Stmt` has mvdan/sh's
|
|
133
|
+
* Background/Negated flag set (`cmd &`, `! cmd`).
|
|
134
|
+
* - `'coproc'` — inside a `CoprocClause`'s statement (a `coproc` block,
|
|
135
|
+
* optionally named).
|
|
136
|
+
*
|
|
137
|
+
* A `Block` grouping (`{ ...; }`) and a `TimeClause` (`time cmd`) are
|
|
138
|
+
* deliberately transparent — grouping and timing a command doesn't change
|
|
139
|
+
* how it's reached, so no frame is added for either.
|
|
140
|
+
*
|
|
141
|
+
* This union may grow in a **minor** release — a future mvdan/sh grammar
|
|
142
|
+
* construct this module starts modeling can add a new `kind` variant
|
|
143
|
+
* without that being a breaking change (mirroring
|
|
144
|
+
* {@link WordResolutionReason}'s semver policy in `resolve-word.ts`). It is
|
|
145
|
+
* deliberately not sealed against extension elsewhere in the codebase.
|
|
146
|
+
* Every existing variant's shape (its extra fields, if any) is stable —
|
|
147
|
+
* only new variants are ever added — so an exhaustive compile-time
|
|
148
|
+
* `switch` over `.kind` should still include a `default` case to stay
|
|
149
|
+
* forward-compatible.
|
|
150
|
+
*
|
|
151
|
+
* @public
|
|
152
|
+
*/
|
|
153
|
+
export declare type CommandContext = {
|
|
154
|
+
readonly kind: 'and';
|
|
155
|
+
readonly side: 'right';
|
|
156
|
+
} | {
|
|
157
|
+
readonly kind: 'or';
|
|
158
|
+
readonly side: 'right';
|
|
159
|
+
} | {
|
|
160
|
+
readonly kind: 'pipeline';
|
|
161
|
+
readonly stage: number;
|
|
162
|
+
} | {
|
|
163
|
+
readonly kind: 'subshell';
|
|
164
|
+
} | {
|
|
165
|
+
readonly kind: 'cmdSubst';
|
|
166
|
+
} | {
|
|
167
|
+
readonly kind: 'procSubst';
|
|
168
|
+
} | {
|
|
169
|
+
readonly kind: 'if';
|
|
170
|
+
readonly branch: 'then' | 'else' | 'cond';
|
|
171
|
+
} | {
|
|
172
|
+
readonly kind: 'case';
|
|
173
|
+
} | {
|
|
174
|
+
readonly kind: 'loop';
|
|
175
|
+
readonly role: 'body' | 'cond';
|
|
176
|
+
} | {
|
|
177
|
+
readonly kind: 'function';
|
|
178
|
+
readonly name: string;
|
|
179
|
+
} | {
|
|
180
|
+
readonly kind: 'background';
|
|
181
|
+
} | {
|
|
182
|
+
readonly kind: 'negated';
|
|
183
|
+
} | {
|
|
184
|
+
readonly kind: 'coproc';
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* One place in the tree where a command is actually invoked — a `CallExpr`
|
|
189
|
+
* node, together with its resolved words and the path used to reach it.
|
|
190
|
+
* Facts only, matching {@link resolveWord}'s posture: no safety verdict, no
|
|
191
|
+
* hardcoded command/wrapper list. A dynamic (`static: false`) `argv0` is a
|
|
192
|
+
* normal, expected result — not an error and not itself reported as
|
|
193
|
+
* "unknown"/"unsafe".
|
|
194
|
+
*
|
|
195
|
+
* @public
|
|
196
|
+
*/
|
|
197
|
+
export declare interface CommandSite {
|
|
198
|
+
/** The `CallExpr` node this site was found at. */
|
|
199
|
+
readonly node: ShNode;
|
|
200
|
+
/**
|
|
201
|
+
* `resolveWord` applied to the first word (`argv[0]`), with
|
|
202
|
+
* `{ context: 'command-argument' }` — every `CallExpr` word is an
|
|
203
|
+
* ordinary command-argument position, never an assignment value, so
|
|
204
|
+
* only a word-initial unquoted `~` triggers tilde expansion (an
|
|
205
|
+
* unquoted `~` after a `:`, e.g. `a:~/b`, is literal text here — see
|
|
206
|
+
* `ResolveWordOptions.context`'s doc comment).
|
|
207
|
+
*/
|
|
208
|
+
readonly argv0: WordResolution;
|
|
209
|
+
/** `resolveWord` applied to every word, in argument order (same `{ context: 'command-argument' }` as {@link CommandSite.argv0}). */
|
|
210
|
+
readonly argv: readonly WordResolution[];
|
|
211
|
+
/** The path from the tree root to this site, outermost frame first. */
|
|
212
|
+
readonly context: readonly CommandContext[];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* `sh-ast/analyze`'s default {@link WrapperSpec} table — every entry's
|
|
217
|
+
* flag/operand handling is hand-derived from that wrapper's own manual
|
|
218
|
+
* page (or, for shell builtins, the Bash Reference Manual/POSIX Shell &
|
|
219
|
+
* Utilities volume), cited per entry below, and cross-checked empirically
|
|
220
|
+
* against the real utility where one was locally available (GNU coreutils
|
|
221
|
+
* `env`/`nice` directly; `sudo` via `sudo -h`'s usage/option summary only
|
|
222
|
+
* — never executed, per this package's "facts only" posture and basic
|
|
223
|
+
* safety hygiene). This table is *data*, not policy: it names no
|
|
224
|
+
* "dangerous" commands, makes no safety judgment, and a caller can freely
|
|
225
|
+
* replace or extend it via {@link ResolveArgv0Options.transparentWrappers}
|
|
226
|
+
* (see {@link resolveArgv0}'s criterion-4-style configurability guarantee).
|
|
227
|
+
*
|
|
228
|
+
* Frozen (including each entry object and its own array/regexp fields) so
|
|
229
|
+
* immutability of this shared, module-scoped table is a contract, not just
|
|
230
|
+
* a convention any importer happens to honor — mirrors `visitorKeys`'s and
|
|
231
|
+
* `CHILD_TYPE_SCHEMA`'s freezing in `visitor-keys.ts`/`normalize.ts`.
|
|
232
|
+
*
|
|
233
|
+
* `xargs` is deliberately **excluded**: unlike every entry here, `xargs`
|
|
234
|
+
* doesn't simply re-exec a single wrapped command word it can point to
|
|
235
|
+
* statically — it invokes `command [initial-arguments]` once per *batch*
|
|
236
|
+
* of additional arguments assembled from its own stdin at run time
|
|
237
|
+
* (batched per `-n`/`-L`/line-splitting rules, GNU xargs(1)). Treating it
|
|
238
|
+
* as transparent would misrepresent what's actually invoked: the "wrapped
|
|
239
|
+
* command" is only ever a syntactic *prefix* of the real, stdin-dependent
|
|
240
|
+
* argv, and that prefix can itself be entirely absent (bare `xargs` reruns
|
|
241
|
+
* each stdin-derived line as its own command). No `WrapperSpec` shape here
|
|
242
|
+
* can honestly express that, so `xargs` is left for a caller to model
|
|
243
|
+
* explicitly if their use case calls for it, rather than shipped as a
|
|
244
|
+
* silently-wrong default.
|
|
245
|
+
*
|
|
246
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Bourne-Shell-Builtins — `command`, `exec` (Bash Reference Manual §4.1)
|
|
247
|
+
* @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html — POSIX `command`
|
|
248
|
+
* @see https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html — GNU coreutils `env`
|
|
249
|
+
* @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/env.html — POSIX `env`
|
|
250
|
+
* @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/nohup.html — POSIX `nohup`
|
|
251
|
+
* @see https://www.gnu.org/software/coreutils/manual/html_node/nice-invocation.html — GNU coreutils `nice`
|
|
252
|
+
* @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/nice.html — POSIX `nice`
|
|
253
|
+
* @see https://pubs.opengroup.org/onlinepubs/9699919799/utilities/time.html — POSIX `time` utility (distinct from the bash `time` *reserved word*, which mvdan/sh models as a transparent `TimeClause` — see {@link WrapperSpec}'s `time` entry below)
|
|
254
|
+
* @see https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html — GNU coreutils `timeout`
|
|
255
|
+
* @see https://www.sudo.ws/docs/man/sudo.man/ — `sudo(8)`
|
|
256
|
+
* @see https://www.gnu.org/software/findutils/manual/html_node/find_html/xargs-options.html — GNU `xargs` (why it's excluded, above)
|
|
257
|
+
* @public
|
|
258
|
+
*/
|
|
259
|
+
export declare const DEFAULT_TRANSPARENT_WRAPPERS: readonly WrapperSpec[];
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Enumerates every command invocation (`CallExpr`) reachable from `root`,
|
|
263
|
+
* with its resolved words ({@link resolveWord}) and the path used to reach
|
|
264
|
+
* it ({@link CommandContext}). Descends everywhere a command can occur:
|
|
265
|
+
* statement lists, both sides of `&&`/`||`/pipelines, subshells/blocks,
|
|
266
|
+
* if/case branches *and* conditions, loop bodies *and* conditions, function
|
|
267
|
+
* bodies, background/negated/coproc statements, and — critically —
|
|
268
|
+
* `CmdSubst`/`ProcSubst` nested inside words, wherever those words occur
|
|
269
|
+
* (arguments, redirection targets, case subjects/patterns, loop word
|
|
270
|
+
* lists, assignment values, test/arithmetic operands, …), not only inside
|
|
271
|
+
* `CallExpr.args`.
|
|
272
|
+
*
|
|
273
|
+
* An assignment-only `CallExpr` (`FOO=bar`, `args` empty) has no first word
|
|
274
|
+
* to resolve and is not itself a command invocation — no program runs — so
|
|
275
|
+
* it produces no {@link CommandSite}; any command substitution nested in
|
|
276
|
+
* its assigned value (`FOO=$(sub)`) is still found and reported.
|
|
277
|
+
*
|
|
278
|
+
* Results are sorted by source position (`node.range[0]`), so nested
|
|
279
|
+
* command substitutions and out-of-structural-order redirection targets
|
|
280
|
+
* still come back in source order regardless of traversal order.
|
|
281
|
+
*
|
|
282
|
+
* Facts only, matching {@link resolveWord}'s posture: no safety verdict, no
|
|
283
|
+
* command/wrapper allowlist or denylist, no dataflow. A statically unknown
|
|
284
|
+
* `argv0` (`static: false`) is a normal, expected result.
|
|
285
|
+
*
|
|
286
|
+
* A long *linear* chain — `|`/`|&`/`&&`/`||` of any realistic length — is
|
|
287
|
+
* traversed iteratively and never risks a stack overflow or trips the
|
|
288
|
+
* nesting-depth guard below, regardless of how many stages/links it has.
|
|
289
|
+
*
|
|
290
|
+
* @param root - Any `ShNode` — typically a `File` from `parseSync`, but any
|
|
291
|
+
* subtree (a `Stmt`, a `Command`, or even a bare `Word`) is handled. Note
|
|
292
|
+
* `parseSync` itself already refuses (via its own `ShParseMaxDepthError`) any
|
|
293
|
+
* input whose estimated nesting exceeds *its* limit before ever producing a
|
|
294
|
+
* tree — see `parse-depth-guard.ts` — so a `root` sourced from `parseSync`
|
|
295
|
+
* has already passed that earlier, lower-level line of defense; this
|
|
296
|
+
* module's own guard below remains a second, independent backstop for
|
|
297
|
+
* trees built or mutated some other way.
|
|
298
|
+
* @throws {@link ShAnalyzeMaxDepthError} if `root`'s genuinely nested
|
|
299
|
+
* structure (subshells within subshells, chained command/process
|
|
300
|
+
* substitutions, deeply nested `if`/`case`/loop/function/`time`/`{ }`
|
|
301
|
+
* bodies, chained `elif`) exceeds this module's defensive recursion-depth
|
|
302
|
+
* guard — see that error's doc comment for why this fails closed instead
|
|
303
|
+
* of returning a partial result, and why a gate-style consumer should treat
|
|
304
|
+
* it as `deny`.
|
|
305
|
+
* @public
|
|
306
|
+
*/
|
|
307
|
+
export declare function enumerateCommands(root: ShNode): CommandSite[];
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* A 1-based line/column position. Columns are UTF-16 code units, matching
|
|
311
|
+
* JavaScript string indexing (see {@link ShNode.range}).
|
|
312
|
+
*
|
|
313
|
+
* @public
|
|
314
|
+
*/
|
|
315
|
+
export declare interface Position {
|
|
316
|
+
readonly line: number;
|
|
317
|
+
readonly column: number;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Follows `site`'s argv0 through zero or more *transparent wrappers* —
|
|
322
|
+
* commands whose own argv0 isn't the command actually run, because they
|
|
323
|
+
* just locate and re-exec another command (`env FOO=1 rm -rf /`,
|
|
324
|
+
* `command rm`, `nohup rm`, `sudo -u x "$prog"`) — to the **effective**
|
|
325
|
+
* command: the one a permission/policy check must actually judge, since
|
|
326
|
+
* argv0 alone is trivially spoofable through any of these.
|
|
327
|
+
*
|
|
328
|
+
* The wrapper table ({@link ResolveArgv0Options.transparentWrappers},
|
|
329
|
+
* defaulting to {@link DEFAULT_TRANSPARENT_WRAPPERS}) is plain data: each
|
|
330
|
+
* {@link WrapperSpec} declares how to locate the word it wraps, and
|
|
331
|
+
* neither this function nor the table encodes any safety judgment about
|
|
332
|
+
* *which* commands are dangerous — matching {@link CommandSite}'s and
|
|
333
|
+
* {@link resolveWord}'s "facts only" posture. A caller can drop a default
|
|
334
|
+
* entry (that name then resolves as an ordinary, non-transparent
|
|
335
|
+
* command) or add their own (e.g. a project's `with-retry` helper) and
|
|
336
|
+
* both directions are followed identically to any built-in entry.
|
|
337
|
+
*
|
|
338
|
+
* **A statically-unknowable word is never guessed through** — three
|
|
339
|
+
* distinct cases all stop the chain immediately at that word rather than
|
|
340
|
+
* falling back to any other guess:
|
|
341
|
+
*
|
|
342
|
+
* 1. `argv0` or any wrapper's located word itself resolves `static: false`
|
|
343
|
+
* (an expansion, tilde, glob, …) — `sudo -u x "$prog"` reports
|
|
344
|
+
* `effective: { static: false, reason: 'expansion' }` with
|
|
345
|
+
* `chain: [<sudo>, <"$prog">]`, not `rm`/`sudo`/anything static.
|
|
346
|
+
* 2. A *statically known* word shaped like a flag (`-`-prefixed, not
|
|
347
|
+
* `--`) doesn't match any flag/operand shape the current
|
|
348
|
+
* {@link WrapperSpec} recognizes — `sudo -D /tmp rm x` (`-D` isn't
|
|
349
|
+
* modeled for `sudo`) reports an `effective` of `static: false` with
|
|
350
|
+
* `reason: 'unknown-flag'`, **not** `rm` (the pre-fix behavior: any
|
|
351
|
+
* unrecognized word, flag-shaped or not, was silently treated as the
|
|
352
|
+
* wrapped command — a false report that hid the real effective
|
|
353
|
+
* command behind a flag this table simply hadn't modeled yet).
|
|
354
|
+
* 3. A recognized flag's value structurally embeds the real command
|
|
355
|
+
* rather than naming it as a separate word — `env -S 'rm -rf /'`
|
|
356
|
+
* reports `effective: { static: false, reason: 'embedded-command' }`
|
|
357
|
+
* (see {@link WrapperSpec.unresolvableFlags}'s doc comment).
|
|
358
|
+
*
|
|
359
|
+
* `VAR=val` shell-assignment prefixes on the `CallExpr` itself
|
|
360
|
+
* (`CommandSite.node.assigns`, e.g. `FOO=bar rm x`) are always skipped and
|
|
361
|
+
* counted in {@link Argv0Resolution.assignmentsSkipped} — a mechanism
|
|
362
|
+
* distinct from a wrapper's own `VAR=val` *operands* (`env A=1 rm x`,
|
|
363
|
+
* `sudo A=1 rm x`), which {@link WrapperSpec.skipAssignmentOperands}
|
|
364
|
+
* handles per-wrapper.
|
|
365
|
+
*
|
|
366
|
+
* @param site - A {@link CommandSite}, typically from {@link enumerateCommands}.
|
|
367
|
+
* @param options - See {@link ResolveArgv0Options}.
|
|
368
|
+
* @throws TypeError if `site.argv` is empty — a programmer-error misuse of
|
|
369
|
+
* the API (every `CommandSite` `enumerateCommands` ever produces has at
|
|
370
|
+
* least one argv word; see its doc comment), not a "malformed shell
|
|
371
|
+
* source" case.
|
|
372
|
+
* @throws {@link ShAnalyzeInvalidWrapperSpecError} if
|
|
373
|
+
* `options.transparentWrappers` contains a malformed entry (see
|
|
374
|
+
* {@link ResolveArgv0Options.transparentWrappers}'s doc comment).
|
|
375
|
+
* @public
|
|
376
|
+
*/
|
|
377
|
+
export declare function resolveArgv0(site: CommandSite, options?: ResolveArgv0Options): Argv0Resolution;
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Options accepted by {@link resolveArgv0}.
|
|
381
|
+
*
|
|
382
|
+
* @public
|
|
383
|
+
*/
|
|
384
|
+
export declare interface ResolveArgv0Options {
|
|
385
|
+
/**
|
|
386
|
+
* The transparent-wrapper table to follow. Defaults to
|
|
387
|
+
* {@link DEFAULT_TRANSPARENT_WRAPPERS}. Pass a replacement array to drop
|
|
388
|
+
* a default entry entirely (that wrapper is then treated as an ordinary,
|
|
389
|
+
* non-transparent effective command), or add project-specific entries
|
|
390
|
+
* (e.g. a `with-retry` wrapper) — the table is plain data, not baked-in
|
|
391
|
+
* policy.
|
|
392
|
+
*
|
|
393
|
+
* Validated at the {@link resolveArgv0} boundary: a malformed entry (a
|
|
394
|
+
* non-array `names`, an empty `names`, or a wrong-typed flag field)
|
|
395
|
+
* throws {@link ShAnalyzeInvalidWrapperSpecError} immediately, rather
|
|
396
|
+
* than failing later with a confusing native `TypeError` deep inside
|
|
397
|
+
* flag matching.
|
|
398
|
+
*/
|
|
399
|
+
readonly transparentWrappers?: readonly WrapperSpec[];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Determines whether a `Word` node is statically a known string, and if
|
|
404
|
+
* so, what that string is — the atom of shell-AST static analysis (e.g.
|
|
405
|
+
* "is this word literally `rm`"). A word is static iff every part is a
|
|
406
|
+
* literal after shell unquoting: a bare `Lit` (backslash escapes
|
|
407
|
+
* processed), a `SglQuoted` (including `$'...'` ANSI-C escapes decoded),
|
|
408
|
+
* or a `DblQuoted` containing only literal parts (and not itself a
|
|
409
|
+
* `$"..."` locale translation) — including concatenations of these (`'r'm`
|
|
410
|
+
* resolves to `"rm"`). A word containing any expansion (`ParamExp`,
|
|
411
|
+
* `CmdSubst`, `ArithmExp`, `ProcSubst`), a triggering unquoted `~` (see
|
|
412
|
+
* `options.context`), an unquoted glob metacharacter/bracket
|
|
413
|
+
* expression/`ExtGlob`, a `$"..."` locale translation, or an unrepresentable
|
|
414
|
+
* `$'\U...'` escape is `static: false` with a reason — never an error, and
|
|
415
|
+
* never a safety judgment (see {@link WordResolution}).
|
|
416
|
+
*
|
|
417
|
+
* @param word - A `Word` node, e.g. `parseSync(...).stmts[0].cmd.args[0]`.
|
|
418
|
+
* A `word` sourced from `parseSync` has already passed that function's own
|
|
419
|
+
* pathological-nesting guard (`ShParseMaxDepthError` — see
|
|
420
|
+
* `parse-depth-guard.ts`), which rejects deeply-nested input before it is
|
|
421
|
+
* ever turned into a tree at all, so `resolveWord` itself never needs (and
|
|
422
|
+
* does not implement) a depth guard of its own.
|
|
423
|
+
* @param options - See {@link ResolveWordOptions}.
|
|
424
|
+
* @throws TypeError if `word.type` is not `"Word"` — a programmer-error
|
|
425
|
+
* misuse of the API, not a "malformed shell source" case (well-formed
|
|
426
|
+
* shell source, however dynamic, never throws; see {@link WordResolution}).
|
|
427
|
+
*
|
|
428
|
+
* @public
|
|
429
|
+
*/
|
|
430
|
+
export declare function resolveWord(word: ShNode, options?: ResolveWordOptions): WordResolution;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Options controlling {@link resolveWord}'s tilde-expansion detection.
|
|
434
|
+
*
|
|
435
|
+
* @public
|
|
436
|
+
*/
|
|
437
|
+
export declare interface ResolveWordOptions {
|
|
438
|
+
/**
|
|
439
|
+
* Which Bash grammar position `word` occupies — the two positions Bash
|
|
440
|
+
* §3.5.2 ("Tilde Expansion") documents distinct tilde-expansion triggers
|
|
441
|
+
* for:
|
|
442
|
+
*
|
|
443
|
+
* - `'command-argument'` — an ordinary simple-command argument word (e.g.
|
|
444
|
+
* `echo <word>`). Only a *word-initial* unquoted `~` triggers tilde
|
|
445
|
+
* expansion; an unquoted `~` elsewhere in the word (including right
|
|
446
|
+
* after a `:`) is an ordinary character (`echo a:~b` prints `a:~b`
|
|
447
|
+
* literally in real bash).
|
|
448
|
+
* - `'assignment-value'` — the value side of a shell assignment (e.g.
|
|
449
|
+
* `PATH=<word>`, an `Assign` node's `value`). Bash additionally
|
|
450
|
+
* tilde-expands an unquoted `~` immediately following an unquoted `:`
|
|
451
|
+
* anywhere in the value (`PATH=/foo:~/bar` expands the `~/bar`
|
|
452
|
+
* segment) — this is documented for `PATH`-like colon-separated
|
|
453
|
+
* assignment values specifically.
|
|
454
|
+
*
|
|
455
|
+
* **Default: `'assignment-value'` semantics apply whenever this option is
|
|
456
|
+
* omitted.** `resolveWord` takes a bare `Word` node with no grammar
|
|
457
|
+
* context attached, so it cannot see whether its caller extracted that
|
|
458
|
+
* word from a command argument or an assignment value — omitting this
|
|
459
|
+
* option is deliberately treated as the *more conservative* of the two
|
|
460
|
+
* (the one that reports `'tilde'` in more cases), so a caller that
|
|
461
|
+
* doesn't know or care about the distinction never under-reports a real
|
|
462
|
+
* tilde-expansion site. Pass `'command-argument'` explicitly to opt into
|
|
463
|
+
* the narrower, word-initial-only check that matches real bash's
|
|
464
|
+
* behavior for ordinary arguments.
|
|
465
|
+
*
|
|
466
|
+
* @see https://www.gnu.org/software/bash/manual/bash.html#Tilde-Expansion
|
|
467
|
+
*/
|
|
468
|
+
readonly context?: 'command-argument' | 'assignment-value';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Thrown by `sh-ast/analyze`'s `resolveArgv0` when a caller-supplied
|
|
473
|
+
* `options.transparentWrappers` array contains a malformed `WrapperSpec`
|
|
474
|
+
* entry — e.g. a non-array or empty `names`, or a flag field that isn't an
|
|
475
|
+
* array of strings. Fails closed with a clear, specific message at the
|
|
476
|
+
* point the malformed table is supplied, rather than letting the bad shape
|
|
477
|
+
* reach flag-matching logic and surface as a confusing native `TypeError`
|
|
478
|
+
* (or, worse, a silently wrong match) far from its actual cause.
|
|
479
|
+
*
|
|
480
|
+
* @public
|
|
481
|
+
*/
|
|
482
|
+
export declare class ShAnalyzeInvalidWrapperSpecError extends ShAstError {
|
|
483
|
+
readonly code = "SH_AST_ANALYZE_INVALID_WRAPPER_SPEC";
|
|
484
|
+
constructor(message: string);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Thrown by `sh-ast/analyze`'s `enumerateCommands` when a tree's genuinely
|
|
489
|
+
* nested structure — subshells within subshells, chained command/process
|
|
490
|
+
* substitutions, deeply nested `if`/`case`/loop/function/`time`/`{ }`
|
|
491
|
+
* bodies, deeply chained `elif` — exceeds its defensive recursion-depth
|
|
492
|
+
* guard. **Not** thrown for a long *linear* chain (`|`/`|&`/`&&`/`||` of
|
|
493
|
+
* any realistic length): `enumerateCommands` traverses those iteratively,
|
|
494
|
+
* so chain length alone never grows this guard's depth counter — only
|
|
495
|
+
* genuine tree nesting does.
|
|
496
|
+
*
|
|
497
|
+
* `enumerateCommands` deliberately fails closed here rather than returning
|
|
498
|
+
* a truncated, partial result: a partial `CommandSite[]` silently omits
|
|
499
|
+
* real command sites, which is a false negative for a permission-hook-style
|
|
500
|
+
* consumer that treats "command not found in the enumeration" as "nothing
|
|
501
|
+
* to worry about" — an explicit, documented throw is safer than a silent
|
|
502
|
+
* under-report. Gate-style consumers should treat this error as `deny`,
|
|
503
|
+
* not fall back to "no commands found, so allow".
|
|
504
|
+
*
|
|
505
|
+
* @public
|
|
506
|
+
*/
|
|
507
|
+
export declare class ShAnalyzeMaxDepthError extends ShAstError {
|
|
508
|
+
readonly code = "SH_AST_ANALYZE_MAX_DEPTH";
|
|
509
|
+
/** The maximum nesting depth `enumerateCommands` supports; the same value every time (not caller-configurable). */
|
|
510
|
+
readonly maxDepth: number;
|
|
511
|
+
constructor(maxDepth: number);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Common base class for every error this package throws — originally just
|
|
516
|
+
* {@link sh-ast#parseSync | parseSync}'s errors, now also the
|
|
517
|
+
* `sh-ast/analyze` layer's (see {@link ShAnalyzeMaxDepthError},
|
|
518
|
+
* {@link ShAnalyzeInvalidWrapperSpecError}). Provides a stable, documented
|
|
519
|
+
* `code` discriminator (e.g. `"SH_AST_PARSE_ERROR"`) alongside the usual
|
|
520
|
+
* `instanceof` narrowing, so consumers can branch on failure kind
|
|
521
|
+
* programmatically without parsing `.message` strings. Never thrown directly
|
|
522
|
+
* — only via its concrete subclasses
|
|
523
|
+
* ({@link sh-ast#ShParseError | ShParseError},
|
|
524
|
+
* {@link sh-ast#ShInvalidDialectError | ShInvalidDialectError},
|
|
525
|
+
* {@link sh-ast#ShInternalError | ShInternalError},
|
|
526
|
+
* {@link ShAnalyzeMaxDepthError},
|
|
527
|
+
* {@link sh-ast#ShParseMaxDepthError | ShParseMaxDepthError},
|
|
528
|
+
* {@link ShAnalyzeInvalidWrapperSpecError}).
|
|
529
|
+
*
|
|
530
|
+
* Re-exported from `sh-ast/analyze` as well as the root `sh-ast` entry
|
|
531
|
+
* point (see #23) — every subclass thrown by `sh-ast/analyze`'s own
|
|
532
|
+
* functions extends this, so a consumer of that subpath alone can
|
|
533
|
+
* `catch`/reference the base without also importing from root.
|
|
534
|
+
*
|
|
535
|
+
* @public
|
|
536
|
+
*/
|
|
537
|
+
export declare abstract class ShAstError extends Error {
|
|
538
|
+
/**
|
|
539
|
+
* Stable, machine-readable discriminator for this error kind. Distinct
|
|
540
|
+
* per subclass and will not change once published.
|
|
541
|
+
*/
|
|
542
|
+
abstract readonly code: string;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* A normalized AST node produced by {@link sh-ast#parseSync | parseSync}.
|
|
547
|
+
*
|
|
548
|
+
* `range` and `loc` are expressed in UTF-16 code units, so
|
|
549
|
+
* `code.slice(node.range[0], node.range[1])` reproduces the node's exact
|
|
550
|
+
* source text — even though mvdan/sh itself reports byte offsets and
|
|
551
|
+
* byte-counting columns internally (see design/ARCHITECTURE.md's
|
|
552
|
+
* "Serialization contract"). Every other field copied over from mvdan/sh's
|
|
553
|
+
* typedjson tree keeps its original value but with the field name
|
|
554
|
+
* lowercased (`Stmts` becomes `stmts`, `CondLast` becomes `condlast`, etc.).
|
|
555
|
+
*
|
|
556
|
+
* Also re-exported from `sh-ast/analyze` (see #23) — the subpath's public
|
|
557
|
+
* surface consumes and exposes `ShNode`, so a consumer can reference the type
|
|
558
|
+
* without also importing from the root `sh-ast` entry point.
|
|
559
|
+
*
|
|
560
|
+
* @public
|
|
561
|
+
*/
|
|
562
|
+
export declare interface ShNode {
|
|
563
|
+
readonly type: string;
|
|
564
|
+
readonly range: readonly [number, number];
|
|
565
|
+
readonly loc: {
|
|
566
|
+
readonly start: Position;
|
|
567
|
+
readonly end: Position;
|
|
568
|
+
};
|
|
569
|
+
[field: string]: unknown;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* The result of statically resolving a `Word` node's text. Reports facts
|
|
574
|
+
* only — never a safety verdict, never a hardcoded command/wrapper list.
|
|
575
|
+
* `static: false` ("unknowable") is a first-class, valid result, not an
|
|
576
|
+
* error condition.
|
|
577
|
+
*
|
|
578
|
+
* @public
|
|
579
|
+
*/
|
|
580
|
+
export declare type WordResolution = {
|
|
581
|
+
/** The word is a compile-time-known string. */
|
|
582
|
+
readonly static: true;
|
|
583
|
+
/** The word's exact resolved text (quotes and escapes removed). */
|
|
584
|
+
readonly text: string;
|
|
585
|
+
} | {
|
|
586
|
+
/** The word's value depends on program state, the environment, or the filesystem. */
|
|
587
|
+
readonly static: false;
|
|
588
|
+
/** Why this word cannot be statically resolved. */
|
|
589
|
+
readonly reason: WordResolutionReason;
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Why a {@link resolveWord} result is `static: false`: the word's exact
|
|
594
|
+
* text cannot be determined from source alone.
|
|
595
|
+
*
|
|
596
|
+
* - `'expansion'` — the word contains a `ParamExp` (`$x`, `${x}`), a
|
|
597
|
+
* `CmdSubst` (command substitution, either `$(...)` or backtick form), an
|
|
598
|
+
* `ArithmExp` (`$((expr))`), or a `ProcSubst` (`<(cmd)`/`>(cmd)`) part:
|
|
599
|
+
* its value depends on program state (variables, command output, process
|
|
600
|
+
* substitution) that only exists at run time.
|
|
601
|
+
* - `'tilde'` — an unquoted, unescaped `~` triggers Bash tilde expansion
|
|
602
|
+
* (Bash Reference Manual §3.5.2, "Tilde Expansion") at the start of the
|
|
603
|
+
* word, or — when `resolveWord`'s `context` option is `'assignment-value'`
|
|
604
|
+
* or omitted — immediately after an unquoted `:` anywhere in the word too
|
|
605
|
+
* (§3.5.2 also documents this for assignment-statement values, e.g.
|
|
606
|
+
* `PATH=/foo:~/bar`). Either way: statically shaped, but its value
|
|
607
|
+
* depends on the filesystem/environment (`$HOME`, NSS user database
|
|
608
|
+
* lookups) at run time. See `resolveWord`'s `context` parameter doc for
|
|
609
|
+
* the full command-argument-vs-assignment-value distinction.
|
|
610
|
+
* - `'glob'` — the word contains an unquoted, unescaped glob metacharacter
|
|
611
|
+
* (`*`/`?`), an unquoted bracket expression (`[...]` — an unquoted `[`
|
|
612
|
+
* with a later unquoted `]` in the same word, POSIX 2.13.1 / Bash
|
|
613
|
+
* Reference Manual §3.5.8.1, "Pattern Matching"), or an `ExtGlob` part
|
|
614
|
+
* (Bash Reference Manual §3.5.8, "Filename Expansion"): its value depends
|
|
615
|
+
* on which filesystem entries exist at run time.
|
|
616
|
+
* - `'brace'` — the word contains a `BraceExp` part (`{a,b}`/`{1..10}`,
|
|
617
|
+
* Bash Reference Manual §3.5.1, "Brace Expansion"). mvdan/sh v3.13.1 only
|
|
618
|
+
* ever produces this node type when its caller applies `syntax.SplitBraces`
|
|
619
|
+
* to the parsed tree; this bridge's shim does not call it, so today a
|
|
620
|
+
* brace expression like `{a,b}` reaches {@link resolveWord} as an
|
|
621
|
+
* ordinary `Lit` (indistinguishable, in the AST this bridge produces,
|
|
622
|
+
* from literal `{`/`,`/`}` characters) rather than as a `BraceExp` part.
|
|
623
|
+
* This reason is handled here for forward-compatibility with that node
|
|
624
|
+
* type and is not reachable through {@link resolveWord} today.
|
|
625
|
+
* - `'locale'` — the word contains a `$"..."` locale-translated string
|
|
626
|
+
* (Bash Reference Manual §3.1.2.5, "Locale-Specific Translation"): its
|
|
627
|
+
* value is looked up in the current locale's translation catalog at run
|
|
628
|
+
* time via `gettext`, regardless of whether its contents also contain an
|
|
629
|
+
* expansion. `'locale'` takes precedence over any `'expansion'` a
|
|
630
|
+
* `$"..."` word's *contents* would otherwise report — the `DblQuoted`
|
|
631
|
+
* node's own `dollar` flag is checked before its children are visited at
|
|
632
|
+
* all, so e.g. `$"hi $x"` reports `'locale'`, never `'expansion'`.
|
|
633
|
+
* - `'unsupported'` — the word contains a `$'...'` (ANSI-C quoted) `\u`/`\U`
|
|
634
|
+
* escape whose value has no corresponding Unicode character (greater than
|
|
635
|
+
* `U+10FFFF`, e.g. `$'\UFFFFFFFF'`): real bash's output for this case is
|
|
636
|
+
* an undocumented, non-UTF-8 byte sequence with no exact representation
|
|
637
|
+
* as a JavaScript string (see `ansi-c-escapes.ts`'s
|
|
638
|
+
* `MAX_UNICODE_CODE_POINT` doc comment for the empirical detail) — this
|
|
639
|
+
* package declines to claim a `static: true` text bash never actually
|
|
640
|
+
* produces.
|
|
641
|
+
*
|
|
642
|
+
* This union may grow in a **minor** release — new mvdan/sh word-part node
|
|
643
|
+
* types, or new statically-shaped-but-dynamic categories, can add new reason
|
|
644
|
+
* strings without that being a breaking change. It is deliberately not
|
|
645
|
+
* sealed against extension elsewhere in the codebase. A reason string this
|
|
646
|
+
* version of the package doesn't yet know about only ever accompanies
|
|
647
|
+
* `static: false`, never changes the meaning of `static: true`, so runtime
|
|
648
|
+
* consumers that only branch on `.static` are safe by construction across
|
|
649
|
+
* such additions; an exhaustive compile-time `switch` over `.reason` should
|
|
650
|
+
* still include a `default` case to stay forward-compatible.
|
|
651
|
+
*
|
|
652
|
+
* @public
|
|
653
|
+
*/
|
|
654
|
+
export declare type WordResolutionReason = 'expansion' | 'tilde' | 'glob' | 'brace' | 'locale' | 'unsupported';
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* A data-only description of one "transparent wrapper" — a command whose
|
|
658
|
+
* own argv0 is not the *effective* command actually run, because it just
|
|
659
|
+
* locates and re-execs another command (`env`, `sudo`, `nohup`, …).
|
|
660
|
+
* {@link resolveArgv0} interprets every field here structurally (skip N
|
|
661
|
+
* words, recognize this literal flag, …); adding a new `WrapperSpec` never
|
|
662
|
+
* requires a code change to {@link resolveArgv0} itself — that's what makes
|
|
663
|
+
* {@link DEFAULT_TRANSPARENT_WRAPPERS} data rather than a set of hardcoded
|
|
664
|
+
* `if (name === 'env')` branches, and what lets a caller extend or replace
|
|
665
|
+
* the table with their own project-specific wrappers (e.g. a `with-retry`
|
|
666
|
+
* helper).
|
|
667
|
+
*
|
|
668
|
+
* Every flag/operand-recognizing field is checked against a word's
|
|
669
|
+
* *statically resolved* text only (its `.text` on a {@link WordResolution}
|
|
670
|
+
* `static: true` result) — a dynamic word can never be identified as one of
|
|
671
|
+
* a wrapper's own flags or operands (see {@link resolveArgv0}'s doc comment
|
|
672
|
+
* for why an unrecognizable word always ends the chain rather than being
|
|
673
|
+
* guessed through).
|
|
674
|
+
*
|
|
675
|
+
* This shape may grow new optional fields in a **minor** release as this
|
|
676
|
+
* bridge models more wrapper flag conventions; existing fields' meaning
|
|
677
|
+
* never changes.
|
|
678
|
+
*
|
|
679
|
+
* @public
|
|
680
|
+
*/
|
|
681
|
+
export declare interface WrapperSpec {
|
|
682
|
+
/**
|
|
683
|
+
* The literal, exact argv0 text (after quote/escape removal) that
|
|
684
|
+
* identifies this wrapper — e.g. `['env']`. Matched only against a
|
|
685
|
+
* `static: true` word; see {@link WrapperSpec}'s doc comment.
|
|
686
|
+
*
|
|
687
|
+
* Matching is **exact-name-only** — never basename matching. `sudo` does
|
|
688
|
+
* not match `/usr/bin/sudo` or `./sudo`; only a word whose *entire*
|
|
689
|
+
* resolved text equals one of `names` is recognized. This is a
|
|
690
|
+
* deliberate, narrower-than-real-world policy choice (a real shell would
|
|
691
|
+
* happily run `/usr/bin/sudo`), not an oversight: silently treating any
|
|
692
|
+
* path *ending in* a known wrapper name as that wrapper would be a
|
|
693
|
+
* different, broader matching policy this package isn't making on a
|
|
694
|
+
* caller's behalf. A caller who wants path-aware matching can express it
|
|
695
|
+
* explicitly via a custom {@link ResolveArgv0Options.transparentWrappers}
|
|
696
|
+
* table (e.g. `names: ['sudo', '/usr/bin/sudo']`).
|
|
697
|
+
*/
|
|
698
|
+
readonly names: readonly string[];
|
|
699
|
+
/**
|
|
700
|
+
* When `true`, a `NAME=value`-shaped operand (an unquoted identifier
|
|
701
|
+
* followed by `=`, e.g. `A=1`) appearing after the wrapper name is
|
|
702
|
+
* skipped — this is `env`'s and `sudo`'s own `VAR=val` *operand*
|
|
703
|
+
* mechanism (setting variables in the wrapped command's environment),
|
|
704
|
+
* which is a distinct, per-wrapper concept from the shell-level
|
|
705
|
+
* `CallExpr.assigns` prefixes {@link Argv0Resolution.assignmentsSkipped}
|
|
706
|
+
* counts.
|
|
707
|
+
*/
|
|
708
|
+
readonly skipAssignmentOperands?: boolean;
|
|
709
|
+
/** Exact flag tokens that take no operand of their own (e.g. `-i`, `-c`). */
|
|
710
|
+
readonly noArgFlags?: readonly string[];
|
|
711
|
+
/**
|
|
712
|
+
* A regular expression matched against a whole flag-position word for
|
|
713
|
+
* flag *shapes* that aren't fixed strings — e.g. `nice`'s legacy attached
|
|
714
|
+
* adjustment form (`nice -10 cmd`). Consumes no separate operand word,
|
|
715
|
+
* like {@link WrapperSpec.noArgFlags}.
|
|
716
|
+
*/
|
|
717
|
+
readonly noArgFlagPattern?: RegExp;
|
|
718
|
+
/**
|
|
719
|
+
* Exact flag tokens that take an operand (e.g. `-u user`, `-n 10`).
|
|
720
|
+
* Recognized in every standard getopt-style form:
|
|
721
|
+
*
|
|
722
|
+
* - Separate word: `-u user` (both words skipped).
|
|
723
|
+
* - Attached short form: `-uuser` (one word — everything after the flag
|
|
724
|
+
* character is the operand text, exactly like real getopt).
|
|
725
|
+
* - Clustered with preceding no-operand short flags:
|
|
726
|
+
* `-Eu user`/`-Euuser` (`-E` from {@link WrapperSpec.noArgFlags},
|
|
727
|
+
* `-u` from here) — see {@link resolveArgv0}'s doc comment for the
|
|
728
|
+
* clustering algorithm.
|
|
729
|
+
* - Attached long form (only for an entry starting with `--`):
|
|
730
|
+
* `--long-flag=value` (one word).
|
|
731
|
+
*
|
|
732
|
+
* Any of these forms is recognized without a separate `WrapperSpec`
|
|
733
|
+
* field — the shapes are derived structurally from `argFlags` and
|
|
734
|
+
* `noArgFlags` together, matching how real short-option parsing works,
|
|
735
|
+
* rather than needing to be spelled out per wrapper.
|
|
736
|
+
*/
|
|
737
|
+
readonly argFlags?: readonly string[];
|
|
738
|
+
/**
|
|
739
|
+
* Exact flag tokens (and, for a `--`-prefixed entry, its attached
|
|
740
|
+
* `--flag=value` form; for a short `-X` entry, its attached `-Xvalue`
|
|
741
|
+
* form) that make the **whole** {@link resolveArgv0} resolution
|
|
742
|
+
* unresolvable the moment they're seen, rather than being skipped like
|
|
743
|
+
* {@link WrapperSpec.argFlags} or ending the chain normally like an
|
|
744
|
+
* unrecognized word. For a flag whose *value itself* structurally embeds
|
|
745
|
+
* the real command in a way no `WrapperSpec` field can name a fixed
|
|
746
|
+
* "the wrapped command is word N" position for — e.g. `env -S string`
|
|
747
|
+
* splices `string`'s own words into argv, so the real command is
|
|
748
|
+
* *inside* that operand, not identifiable as a separate word at all (see
|
|
749
|
+
* `env`'s table entry). Reported via
|
|
750
|
+
* {@link Argv0Resolution.effective}'s `'embedded-command'` reason.
|
|
751
|
+
*/
|
|
752
|
+
readonly unresolvableFlags?: readonly string[];
|
|
753
|
+
/**
|
|
754
|
+
* Exact flag tokens whose mere presence means this wrapper's own
|
|
755
|
+
* invocation *is* the effective command — nothing after it is ever the
|
|
756
|
+
* wrapped command, regardless of what other words follow. E.g.
|
|
757
|
+
* `command -v rm` doesn't execute `rm` at all; it prints whether `rm`
|
|
758
|
+
* is a recognized command name (Bash Reference Manual §4.1). Checked
|
|
759
|
+
* independently of word position (not "the Nth flag"), and takes
|
|
760
|
+
* precedence the moment it's seen — see `command`'s table entry.
|
|
761
|
+
*/
|
|
762
|
+
readonly stopsChainFlags?: readonly string[];
|
|
763
|
+
/**
|
|
764
|
+
* The number of plain (non-flag, non-assignment-operand) words this
|
|
765
|
+
* wrapper requires *before* the wrapped command word — e.g. `timeout`'s
|
|
766
|
+
* mandatory `DURATION` operand (`timeout 10 rm -rf x`).
|
|
767
|
+
*/
|
|
768
|
+
readonly positionalOperandsBeforeCommand?: number;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export { }
|