@gotgenes/pi-permission-system 27.1.0 → 27.1.2
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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [27.1.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v27.1.1...pi-permission-system-v27.1.2) (2026-08-29)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **pi-permission-system:** claim no arity for awk's long forms, whose parser the bare name does not fix ([#823](https://github.com/gotgenes/pi-packages/issues/823)) ([6c8e4a0](https://github.com/gotgenes/pi-packages/commit/6c8e4a0b813d17750296cbd065aec20377c82d2a))
|
|
14
|
+
* **pi-permission-system:** discharge a flag argument on whatever node type follows ([#823](https://github.com/gotgenes/pi-packages/issues/823)) ([228f6fb](https://github.com/gotgenes/pi-packages/commit/228f6fb235310b304557ec501567373e33d4cfab))
|
|
15
|
+
* **pi-permission-system:** read --context per tool, whose arity differs between grep and rg ([#823](https://github.com/gotgenes/pi-packages/issues/823)) ([bba8317](https://github.com/gotgenes/pi-packages/commit/bba83175d1f9f8079cd9613412c65868e4a2fa09))
|
|
16
|
+
* **pi-permission-system:** read awk's long forms only for gawk ([#823](https://github.com/gotgenes/pi-packages/issues/823)) ([52488f0](https://github.com/gotgenes/pi-packages/commit/52488f02cc902b8669f893cc1cfee090482f2e72))
|
|
17
|
+
* **pi-permission-system:** recognize the long, embedded, and glued spellings of a pattern-first flag ([#823](https://github.com/gotgenes/pi-packages/issues/823)) ([ce7b65e](https://github.com/gotgenes/pi-packages/commit/ce7b65eb607cc2e6f61a6eceda1d1a9bb22aea85))
|
|
18
|
+
* **pi-permission-system:** spend a pattern positional on a computed or numeric pattern ([#823](https://github.com/gotgenes/pi-packages/issues/823)) ([dce4d3f](https://github.com/gotgenes/pi-packages/commit/dce4d3f06b164f6f45df3c5550ec624f3acba896))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
* **pi-permission-system:** record the optional-argument flag residuals ([#823](https://github.com/gotgenes/pi-packages/issues/823)) ([94e51fc](https://github.com/gotgenes/pi-packages/commit/94e51fc5a05acf3b2c4bc49479b45b0b7797b9ac))
|
|
24
|
+
|
|
25
|
+
## [27.1.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v27.1.0...pi-permission-system-v27.1.1) (2026-08-28)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
* **pi-permission-system:** project glob-bearing bash path tokens to the path surfaces ([706c75d](https://github.com/gotgenes/pi-packages/commit/706c75da10d03c61ef0d53f9571d249b91986db5)), closes [#821](https://github.com/gotgenes/pi-packages/issues/821)
|
|
31
|
+
|
|
8
32
|
## [27.1.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v27.0.1...pi-permission-system-v27.1.0) (2026-08-27)
|
|
9
33
|
|
|
10
34
|
|
package/package.json
CHANGED
|
@@ -16,9 +16,17 @@
|
|
|
16
16
|
* `docs/decisions/0009-bash-path-projection-completeness-contract.md`.
|
|
17
17
|
*
|
|
18
18
|
* All three classifiers share the private `rejectNonPathToken` predicate that
|
|
19
|
-
* captures the
|
|
19
|
+
* captures the five rejection cases common to them (the production clone this
|
|
20
20
|
* module was extracted to eliminate).
|
|
21
21
|
*
|
|
22
|
+
* None of them reads a token's glob or regex metacharacters. A shell bracket
|
|
23
|
+
* glob and a regex character class are spelled identically, so "contains a
|
|
24
|
+
* metacharacter" cannot decide path-hood — and it silently dropped tokens the
|
|
25
|
+
* shell really does expand into filesystem paths (`/etc/[p]asswd`,
|
|
26
|
+
* `rm -rf /tmp/tmp.*`, #821). What a pattern argument is instead settled by
|
|
27
|
+
* *position*: `PATTERN_FIRST_COMMANDS` in `token-collection.ts` skips a
|
|
28
|
+
* pattern-first command's inline pattern positional at collection time.
|
|
29
|
+
*
|
|
22
30
|
* Both `classifyTokenAsPathCandidate` and `classifyTokenAsRuleCandidate` recognize
|
|
23
31
|
* Windows drive-letter absolute paths (`C:/…`, `C:\…`) unconditionally on all
|
|
24
32
|
* platforms. On POSIX the token resolves as a real in-CWD relative path and is
|
|
@@ -106,8 +114,8 @@ export function classifyTokenAsRuleCandidate(
|
|
|
106
114
|
* *shape* name a path at all?
|
|
107
115
|
*
|
|
108
116
|
* It runs only the shared `rejectNonPathToken` prelude, so a flag,
|
|
109
|
-
* env-assignment, URL, `@scope` token
|
|
110
|
-
*
|
|
117
|
+
* env-assignment, URL, or `@scope` token is never a candidate. Everything else
|
|
118
|
+
* is returned for the caller to probe.
|
|
111
119
|
*
|
|
112
120
|
* Deliberately consults no policy: candidacy is settled by the filesystem and
|
|
113
121
|
* the decision by the ruleset, which keeps this module a pure shape function
|
|
@@ -137,19 +145,14 @@ const WINDOWS_DRIVE_PATH_PATTERN = /^[a-zA-Z]:[/\\]/;
|
|
|
137
145
|
*/
|
|
138
146
|
const URL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
139
147
|
|
|
140
|
-
/**
|
|
141
|
-
* Regex metacharacter sequences that are never found in real filesystem paths.
|
|
142
|
-
* If a token contains any of these, it is almost certainly a regex pattern
|
|
143
|
-
* (e.g. a grep argument) rather than a path.
|
|
144
|
-
*/
|
|
145
|
-
const REGEX_METACHAR_PATTERN = /\.\*|\.\+|\\\||\\\(|\\\)|\[.*?\]|\^\//;
|
|
146
|
-
|
|
147
148
|
/**
|
|
148
149
|
* Shared rejection prelude: returns `true` when a token can never be a
|
|
149
150
|
* filesystem path, regardless of which classifier is asking.
|
|
150
151
|
*
|
|
151
152
|
* Rejects: empty tokens, flags (leading `-`), env assignments (`FOO=/bar`),
|
|
152
|
-
* URLs, `@scope/package` patterns
|
|
153
|
+
* URLs, and `@scope/package` patterns. Each rules a token out by syntax; a
|
|
154
|
+
* glob or regex metacharacter does not, since the two are indistinguishable
|
|
155
|
+
* and the shell expands the former into real paths (#821).
|
|
153
156
|
*
|
|
154
157
|
* A bare `/` (or `//`, `///`) is NOT rejected: it denotes the filesystem root,
|
|
155
158
|
* a deliberate external-directory access (`find /`, `ls /`), so it must reach
|
|
@@ -172,7 +175,5 @@ function rejectNonPathToken(token: string): boolean {
|
|
|
172
175
|
// since it looks like an absolute-rooted path, not an npm scope.
|
|
173
176
|
if (token.startsWith("@") && !token.startsWith("@/")) return true;
|
|
174
177
|
|
|
175
|
-
if (REGEX_METACHAR_PATTERN.test(token)) return true;
|
|
176
|
-
|
|
177
178
|
return false;
|
|
178
179
|
}
|
|
@@ -79,10 +79,11 @@ export function collectCommandTokens(node: TSNode): PathToken[] {
|
|
|
79
79
|
const config = commandName
|
|
80
80
|
? PATTERN_FIRST_COMMANDS.get(commandName)
|
|
81
81
|
: undefined;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
if (config) return collectPatternCommandTokens(node, config, effect);
|
|
83
|
+
return [
|
|
84
|
+
...collectGenericCommandTokens(node, effect),
|
|
85
|
+
...collectEmbeddedOptionValues(node, effect),
|
|
86
|
+
];
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/**
|
|
@@ -208,11 +209,11 @@ function commandArgumentWords(node: TSNode): string[] {
|
|
|
208
209
|
const OPTION_VALUE_PATTERN = /^-{1,2}[^=\s]+=(.+)$/;
|
|
209
210
|
|
|
210
211
|
/**
|
|
211
|
-
* The values embedded in
|
|
212
|
+
* The values embedded in a **generic** command's `--opt=value` argument tokens.
|
|
212
213
|
*
|
|
213
214
|
* Read straight from the argument nodes rather than from the collected token
|
|
214
|
-
* list, because a
|
|
215
|
-
*
|
|
215
|
+
* list, because a collector classifies a flag and never emits it — so
|
|
216
|
+
* `tar --directory=/etc` would otherwise lose the path.
|
|
216
217
|
*
|
|
217
218
|
* This is token *preprocessing*, not classification: the extracted value is
|
|
218
219
|
* handed to the ordinary shape classifiers and existence probe, so
|
|
@@ -220,6 +221,10 @@ const OPTION_VALUE_PATTERN = /^-{1,2}[^=\s]+=(.+)$/;
|
|
|
220
221
|
* yields a bare `json` that names nothing and is dropped. Keeping the split
|
|
221
222
|
* here is what lets the projection see option-embedded paths without per-command
|
|
222
223
|
* option tables (ADR 0009, #645).
|
|
224
|
+
*
|
|
225
|
+
* A pattern-first command runs the same split from inside its own walker
|
|
226
|
+
* instead, because there the flag's *role* is known: splitting blindly emits a
|
|
227
|
+
* pattern flag's value as a path candidate (#823).
|
|
223
228
|
*/
|
|
224
229
|
function collectEmbeddedOptionValues(
|
|
225
230
|
node: TSNode,
|
|
@@ -239,11 +244,64 @@ function collectEmbeddedOptionValues(
|
|
|
239
244
|
return values;
|
|
240
245
|
}
|
|
241
246
|
|
|
247
|
+
/** The value embedded in a single `--opt=value` token, if it carries one. */
|
|
248
|
+
function embeddedOptionValueToken(
|
|
249
|
+
text: string,
|
|
250
|
+
effect: TokenEffect,
|
|
251
|
+
): PathToken[] {
|
|
252
|
+
const value = OPTION_VALUE_PATTERN.exec(text)?.[1];
|
|
253
|
+
return value === undefined ? [] : [{ token: value, effect }];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* What a recognized flag's argument is, for the pattern-first walker.
|
|
258
|
+
*
|
|
259
|
+
* `script` and `script-file` mark the inline pattern positional as already
|
|
260
|
+
* supplied; `value` and `suffix` do not. Only `script-file` contributes a path
|
|
261
|
+
* candidate — the others name a pattern, a count, or a backup suffix.
|
|
262
|
+
*/
|
|
263
|
+
type PatternFlagRole =
|
|
264
|
+
/** Supplies the pattern/script inline (`grep -e`, `sed --expression`). */
|
|
265
|
+
| "script"
|
|
266
|
+
/** Supplies the pattern/script from a file (`grep -f`, `sed --file`). */
|
|
267
|
+
| "script-file"
|
|
268
|
+
/** Consumes a value that is neither pattern nor path (`grep -A`, `rg -g`). */
|
|
269
|
+
| "value"
|
|
270
|
+
/**
|
|
271
|
+
* Consumes the following argument only when it is empty.
|
|
272
|
+
*
|
|
273
|
+
* BSD `sed` requires a separate suffix argument (`sed -i '' 's/a/b/' f`)
|
|
274
|
+
* while GNU `sed` requires it glued (`-i`, `-i.bak`). Consuming
|
|
275
|
+
* unconditionally is right for one and eats the *script* on the other,
|
|
276
|
+
* leaving the file operand to be skipped as the inline pattern — a write
|
|
277
|
+
* target that reaches no path surface. The argument's own emptiness decides
|
|
278
|
+
* it, so the walk needs no knowledge of which sed is installed (#823).
|
|
279
|
+
*
|
|
280
|
+
* BSD also accepts a separate *non-empty* suffix (`sed -i bak 's/a/b/' f`),
|
|
281
|
+
* which this rule declines: the suffix then spends the pattern positional
|
|
282
|
+
* and the script surfaces as a candidate. The file operand still survives,
|
|
283
|
+
* so the residual is on ADR 0009's recoverable side.
|
|
284
|
+
*/
|
|
285
|
+
| "suffix"
|
|
286
|
+
/**
|
|
287
|
+
* Recognized, but whose arity depends on which implementation the command's
|
|
288
|
+
* *name* resolves to — so it takes neither the following argument nor the
|
|
289
|
+
* pattern positional.
|
|
290
|
+
*
|
|
291
|
+
* `awk` is GNU awk on Fedora/RHEL, where `--file prog.awk` reads `prog.awk`,
|
|
292
|
+
* and one-true-awk or mawk elsewhere, where the long option is ignored
|
|
293
|
+
* outright and `prog.awk` is the program *text*. Asserting either arity
|
|
294
|
+
* drops a real operand on the other family, and the projection cannot see
|
|
295
|
+
* which binary the name will reach. Claiming neither over-surfaces on both
|
|
296
|
+
* — the recoverable direction — and the extra token names nothing, so the
|
|
297
|
+
* existence probe discards it. Prefer a precise role wherever the name does
|
|
298
|
+
* fix the parser: `gawk` gets the real ones (#823).
|
|
299
|
+
*/
|
|
300
|
+
| "unknown-arity";
|
|
301
|
+
|
|
242
302
|
interface PatternCommandConfig {
|
|
243
|
-
/**
|
|
244
|
-
readonly
|
|
245
|
-
/** Flags that consume the next argument as a file path */
|
|
246
|
-
readonly fileConsumingFlags: ReadonlySet<string>;
|
|
303
|
+
/** Recognized flag spellings, short and long, mapped to their roles. */
|
|
304
|
+
readonly flags: ReadonlyMap<string, PatternFlagRole>;
|
|
247
305
|
/**
|
|
248
306
|
* Number of leading positional arguments that are patterns/scripts, not paths.
|
|
249
307
|
* Default: 1 (covers sed, awk, grep, rg).
|
|
@@ -252,130 +310,186 @@ interface PatternCommandConfig {
|
|
|
252
310
|
readonly patternPositionals?: number;
|
|
253
311
|
}
|
|
254
312
|
|
|
313
|
+
const GREP_FLAGS = new Map<string, PatternFlagRole>([
|
|
314
|
+
["-e", "script"],
|
|
315
|
+
["--regexp", "script"],
|
|
316
|
+
["-f", "script-file"],
|
|
317
|
+
["--file", "script-file"],
|
|
318
|
+
["-A", "value"],
|
|
319
|
+
["--after-context", "value"],
|
|
320
|
+
["-B", "value"],
|
|
321
|
+
["--before-context", "value"],
|
|
322
|
+
["-C", "value"],
|
|
323
|
+
// `--context` is deliberately absent, though `-C` is present and `rg` lists
|
|
324
|
+
// the long form below. grep parses with getopt, which declares `context`
|
|
325
|
+
// with an *optional* argument (`-C[NUM]`'s history), and a long option
|
|
326
|
+
// declared that way never takes a separate `argv`: `grep --context 2 pat f`
|
|
327
|
+
// searches for `2` in the files `pat` and `f`. Listing it would consume the
|
|
328
|
+
// `2`, leaving `pat` — a real file operand — to be skipped as the inline
|
|
329
|
+
// pattern. Its absence costs only a bare `2` token from `--context=2`, which
|
|
330
|
+
// names nothing and the existence probe drops (#823).
|
|
331
|
+
["-m", "value"],
|
|
332
|
+
["--max-count", "value"],
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const SED_CONFIG: PatternCommandConfig = {
|
|
336
|
+
flags: new Map<string, PatternFlagRole>([
|
|
337
|
+
["-e", "script"],
|
|
338
|
+
["--expression", "script"],
|
|
339
|
+
["-f", "script-file"],
|
|
340
|
+
["--file", "script-file"],
|
|
341
|
+
["-i", "suffix"],
|
|
342
|
+
]),
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The short flags are POSIX and consume on every awk; the GNU long forms are
|
|
347
|
+
* `unknown-arity` because the bare name does not fix the parser.
|
|
348
|
+
*
|
|
349
|
+
* `awk` is GNU awk on Fedora/RHEL, where `--file prog.awk` reads `prog.awk`,
|
|
350
|
+
* and one-true-awk or mawk on macOS and Debian/Ubuntu, where the long option
|
|
351
|
+
* is ignored outright (`awk: unknown option --field-separator ignored`) and
|
|
352
|
+
* the following words are the program text and its input files. Asserting
|
|
353
|
+
* either arity drops a real operand on the other family, so the table asserts
|
|
354
|
+
* neither. `nawk` shares this for the same reason (#823).
|
|
355
|
+
*/
|
|
356
|
+
const AWK_CONFIG: PatternCommandConfig = {
|
|
357
|
+
flags: new Map<string, PatternFlagRole>([
|
|
358
|
+
["-e", "script"],
|
|
359
|
+
["-f", "script-file"],
|
|
360
|
+
["-F", "value"],
|
|
361
|
+
["-v", "value"],
|
|
362
|
+
["--source", "unknown-arity"],
|
|
363
|
+
["--file", "unknown-arity"],
|
|
364
|
+
["--field-separator", "unknown-arity"],
|
|
365
|
+
["--assign", "unknown-arity"],
|
|
366
|
+
]),
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/** `gawk` names GNU awk outright, so its long forms carry their real roles. */
|
|
370
|
+
const GAWK_CONFIG: PatternCommandConfig = {
|
|
371
|
+
flags: new Map<string, PatternFlagRole>([
|
|
372
|
+
...AWK_CONFIG.flags,
|
|
373
|
+
["--source", "script"],
|
|
374
|
+
["--file", "script-file"],
|
|
375
|
+
["--field-separator", "value"],
|
|
376
|
+
["--assign", "value"],
|
|
377
|
+
]),
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const GREP_CONFIG: PatternCommandConfig = { flags: GREP_FLAGS };
|
|
381
|
+
|
|
382
|
+
const RG_CONFIG: PatternCommandConfig = {
|
|
383
|
+
flags: new Map<string, PatternFlagRole>([
|
|
384
|
+
...GREP_FLAGS,
|
|
385
|
+
// rg parses with clap rather than getopt, where `--context` takes a
|
|
386
|
+
// required argument, so its separated spelling really does consume:
|
|
387
|
+
// `rg --context 2 pat f` searches for `pat` in `f`. Same spelling as
|
|
388
|
+
// grep's, opposite arity — which is why it is listed per tool rather than
|
|
389
|
+
// shared above (#823).
|
|
390
|
+
["--context", "value"],
|
|
391
|
+
["-g", "value"],
|
|
392
|
+
["--glob", "value"],
|
|
393
|
+
["-t", "value"],
|
|
394
|
+
["--type", "value"],
|
|
395
|
+
["-T", "value"],
|
|
396
|
+
["--type-not", "value"],
|
|
397
|
+
["-j", "value"],
|
|
398
|
+
["--threads", "value"],
|
|
399
|
+
["-M", "value"],
|
|
400
|
+
["--max-columns", "value"],
|
|
401
|
+
["-r", "value"],
|
|
402
|
+
["--replace", "value"],
|
|
403
|
+
["-E", "value"],
|
|
404
|
+
["--encoding", "value"],
|
|
405
|
+
]),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const SD_CONFIG: PatternCommandConfig = {
|
|
409
|
+
flags: new Map<string, PatternFlagRole>([
|
|
410
|
+
["-f", "value"],
|
|
411
|
+
["--flags", "value"],
|
|
412
|
+
["-n", "value"],
|
|
413
|
+
["--max-replacements", "value"],
|
|
414
|
+
]),
|
|
415
|
+
patternPositionals: 2,
|
|
416
|
+
};
|
|
417
|
+
|
|
255
418
|
/**
|
|
256
419
|
* Commands whose first N positional arguments are inline patterns/scripts,
|
|
257
420
|
* not filesystem paths. The map stores per-command flag configuration so
|
|
258
421
|
* the walker can correctly identify which arguments are consumed by flags
|
|
259
422
|
* vs. which are positional.
|
|
423
|
+
*
|
|
424
|
+
* Names share a configuration object only when they share a *parser*, which is
|
|
425
|
+
* narrower than being aliases: `egrep`/`fgrep` are the same binary as `grep`
|
|
426
|
+
* here, and `nawk` is one-true-awk like `awk` — but `gawk` has its own config,
|
|
427
|
+
* because it is the only one of the three that certainly means GNU awk and so
|
|
428
|
+
* the only one whose long options certainly consume (#823).
|
|
260
429
|
*/
|
|
261
430
|
const PATTERN_FIRST_COMMANDS: ReadonlyMap<string, PatternCommandConfig> =
|
|
262
431
|
new Map([
|
|
263
|
-
[
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
],
|
|
270
|
-
[
|
|
271
|
-
|
|
272
|
-
{
|
|
273
|
-
argConsumingFlags: new Set(["-e", "-F", "-v"]),
|
|
274
|
-
fileConsumingFlags: new Set(["-f"]),
|
|
275
|
-
},
|
|
276
|
-
],
|
|
277
|
-
[
|
|
278
|
-
"gawk",
|
|
279
|
-
{
|
|
280
|
-
argConsumingFlags: new Set(["-e", "-F", "-v"]),
|
|
281
|
-
fileConsumingFlags: new Set(["-f"]),
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
|
-
[
|
|
285
|
-
"nawk",
|
|
286
|
-
{
|
|
287
|
-
argConsumingFlags: new Set(["-e", "-F", "-v"]),
|
|
288
|
-
fileConsumingFlags: new Set(["-f"]),
|
|
289
|
-
},
|
|
290
|
-
],
|
|
291
|
-
[
|
|
292
|
-
"grep",
|
|
293
|
-
{
|
|
294
|
-
argConsumingFlags: new Set(["-e", "-A", "-B", "-C", "-m"]),
|
|
295
|
-
fileConsumingFlags: new Set(["-f"]),
|
|
296
|
-
},
|
|
297
|
-
],
|
|
298
|
-
[
|
|
299
|
-
"egrep",
|
|
300
|
-
{
|
|
301
|
-
argConsumingFlags: new Set(["-e", "-A", "-B", "-C", "-m"]),
|
|
302
|
-
fileConsumingFlags: new Set(["-f"]),
|
|
303
|
-
},
|
|
304
|
-
],
|
|
305
|
-
[
|
|
306
|
-
"fgrep",
|
|
307
|
-
{
|
|
308
|
-
argConsumingFlags: new Set(["-e", "-A", "-B", "-C", "-m"]),
|
|
309
|
-
fileConsumingFlags: new Set(["-f"]),
|
|
310
|
-
},
|
|
311
|
-
],
|
|
312
|
-
[
|
|
313
|
-
"rg",
|
|
314
|
-
{
|
|
315
|
-
argConsumingFlags: new Set([
|
|
316
|
-
"-e",
|
|
317
|
-
"-A",
|
|
318
|
-
"-B",
|
|
319
|
-
"-C",
|
|
320
|
-
"-m",
|
|
321
|
-
"-g",
|
|
322
|
-
"-t",
|
|
323
|
-
"-T",
|
|
324
|
-
"-j",
|
|
325
|
-
"-M",
|
|
326
|
-
"-r",
|
|
327
|
-
"-E",
|
|
328
|
-
]),
|
|
329
|
-
fileConsumingFlags: new Set(["-f"]),
|
|
330
|
-
},
|
|
331
|
-
],
|
|
332
|
-
[
|
|
333
|
-
"sd",
|
|
334
|
-
{
|
|
335
|
-
argConsumingFlags: new Set(["-n", "-f"]),
|
|
336
|
-
fileConsumingFlags: new Set([]),
|
|
337
|
-
patternPositionals: 2,
|
|
338
|
-
},
|
|
339
|
-
],
|
|
432
|
+
["sed", SED_CONFIG],
|
|
433
|
+
["awk", AWK_CONFIG],
|
|
434
|
+
["gawk", GAWK_CONFIG],
|
|
435
|
+
["nawk", AWK_CONFIG],
|
|
436
|
+
["grep", GREP_CONFIG],
|
|
437
|
+
["egrep", GREP_CONFIG],
|
|
438
|
+
["fgrep", GREP_CONFIG],
|
|
439
|
+
["rg", RG_CONFIG],
|
|
440
|
+
["sd", SD_CONFIG],
|
|
340
441
|
]);
|
|
341
442
|
|
|
342
443
|
/**
|
|
343
444
|
* Describes what the walker should do when it encounters a flag word inside
|
|
344
445
|
* a pattern-first command. Using a discriminated union lets the `switch` in
|
|
345
|
-
* `collectPatternCommandTokens` narrow
|
|
446
|
+
* `collectPatternCommandTokens` narrow the flag's role without a non-null
|
|
346
447
|
* assertion (which would trigger the Biome/ESLint assertion conflict).
|
|
347
448
|
*/
|
|
348
449
|
type PatternCommandFlagDirective =
|
|
349
450
|
| { kind: "end-of-flags" }
|
|
350
451
|
| { kind: "regular-flag" }
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
452
|
+
/** A recognized flag whose value is the argument that follows it. */
|
|
453
|
+
| { kind: "consume-next"; role: PatternFlagRole }
|
|
454
|
+
/** A recognized flag carrying its value in the same token. */
|
|
455
|
+
| { kind: "inline-value"; role: PatternFlagRole; value: string };
|
|
456
|
+
|
|
457
|
+
/** A long option carrying its value inline: `--name=value`. */
|
|
458
|
+
const LONG_OPTION_VALUE_PATTERN = /^(--[^=\s]+)=(.+)$/;
|
|
356
459
|
|
|
357
460
|
/**
|
|
358
461
|
* Classify a flag word from a pattern-first command into a directive that
|
|
359
|
-
* tells the walker how to handle the flag and its
|
|
462
|
+
* tells the walker how to handle the flag and its value.
|
|
463
|
+
*
|
|
464
|
+
* Matched in the order the tools accept: the exact spelling (short or long),
|
|
465
|
+
* then a long option's `=`-embedded value, then a glued short value. The glued
|
|
466
|
+
* form matches only the **first** short flag, which is getopt's own rule —
|
|
467
|
+
* `grep -ei pattern` really is `-e` with the value `i`. A cluster whose
|
|
468
|
+
* argument-taking flag is not first (`grep -ie pattern`) therefore stays a
|
|
469
|
+
* plain flag, which over-surfaces the pattern rather than dropping the
|
|
470
|
+
* command's operand (ADR 0009's recoverable direction).
|
|
360
471
|
*/
|
|
361
472
|
function classifyPatternCommandFlag(
|
|
362
473
|
text: string,
|
|
363
474
|
config: PatternCommandConfig,
|
|
364
475
|
): PatternCommandFlagDirective {
|
|
365
476
|
if (text === "--") return { kind: "end-of-flags" };
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
477
|
+
|
|
478
|
+
const exact = config.flags.get(text);
|
|
479
|
+
if (exact) return { kind: "consume-next", role: exact };
|
|
480
|
+
|
|
481
|
+
const longOption = LONG_OPTION_VALUE_PATTERN.exec(text);
|
|
482
|
+
if (longOption) {
|
|
483
|
+
const [, name, value] = longOption;
|
|
484
|
+
const role = config.flags.get(name);
|
|
485
|
+
return role === undefined
|
|
486
|
+
? { kind: "regular-flag" }
|
|
487
|
+
: { kind: "inline-value", role, value };
|
|
372
488
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
setsExplicitScript: true,
|
|
378
|
-
};
|
|
489
|
+
|
|
490
|
+
if (!text.startsWith("--") && text.length > 2) {
|
|
491
|
+
const role = config.flags.get(text.slice(0, 2));
|
|
492
|
+
if (role) return { kind: "inline-value", role, value: text.slice(2) };
|
|
379
493
|
}
|
|
380
494
|
return { kind: "regular-flag" };
|
|
381
495
|
}
|
|
@@ -389,11 +503,13 @@ function classifyPatternCommandFlag(
|
|
|
389
503
|
* inline patterns/scripts and are skipped. Remaining positional
|
|
390
504
|
* arguments are collected as path candidates.
|
|
391
505
|
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
* flag, so
|
|
506
|
+
* A recognized flag's role (see {@link PatternFlagRole}) decides three things
|
|
507
|
+
* at once: whether the pattern positional is still expected, whether the
|
|
508
|
+
* flag's value is a path candidate, and — for `suffix` — whether the
|
|
509
|
+
* following argument belongs to the flag at all. The `=`-embedded and glued
|
|
510
|
+
* spellings carry the value in the flag's own token, so the walker splits it
|
|
511
|
+
* here rather than letting {@link collectEmbeddedOptionValues} emit a
|
|
512
|
+
* pattern's text as a path (#823).
|
|
397
513
|
*/
|
|
398
514
|
function collectPatternCommandTokens(
|
|
399
515
|
node: TSNode,
|
|
@@ -403,7 +519,7 @@ function collectPatternCommandTokens(
|
|
|
403
519
|
const patternPositionals = config.patternPositionals ?? 1;
|
|
404
520
|
let hasExplicitScript = false;
|
|
405
521
|
let positionalsSeen = 0;
|
|
406
|
-
let
|
|
522
|
+
let pendingConsumption: PatternFlagRole | null = null;
|
|
407
523
|
let pastEndOfFlags = false;
|
|
408
524
|
const tokens: PathToken[] = [];
|
|
409
525
|
|
|
@@ -415,23 +531,50 @@ function collectPatternCommandTokens(
|
|
|
415
531
|
if (child.type === "command_name" || child.type === "variable_assignment")
|
|
416
532
|
continue;
|
|
417
533
|
|
|
418
|
-
|
|
419
|
-
// (e.g. command_substitution) for nested commands.
|
|
420
|
-
if (!ARG_NODE_TYPES.has(child.type)) {
|
|
421
|
-
tokens.push(...collectPathCandidateTokens(child));
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
|
|
534
|
+
const isArgNode = ARG_NODE_TYPES.has(child.type);
|
|
425
535
|
const text = resolveNodeText(child);
|
|
426
536
|
|
|
427
|
-
// Handle
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
537
|
+
// Handle the argument a previous flag consumed. The consumption discharges
|
|
538
|
+
// on whatever node type follows, not only on an ARG_NODE_TYPES one: a bare
|
|
539
|
+
// number (`-A 3`), an expansion (`-A $N`), and a substitution
|
|
540
|
+
// (`-A $(echo 3)`) are all this flag's argument, and carrying the pending
|
|
541
|
+
// skip past them lands it on the *pattern* — shifting the positional count
|
|
542
|
+
// by one and eating the command's real file operand (#823).
|
|
543
|
+
if (pendingConsumption !== null) {
|
|
544
|
+
const consumption = pendingConsumption;
|
|
545
|
+
pendingConsumption = null;
|
|
546
|
+
if (!isArgNode) {
|
|
547
|
+
// Contributes no operand text of its own, but may host a nested
|
|
548
|
+
// execution whose operands are candidates (#741).
|
|
549
|
+
tokens.push(...collectPathCandidateTokens(child));
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const discharge = dischargePendingConsumption(consumption, text, effect);
|
|
553
|
+
if (discharge.token) tokens.push(discharge.token);
|
|
554
|
+
if (discharge.consumed) continue;
|
|
431
555
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
556
|
+
|
|
557
|
+
// A node outside ARG_NODE_TYPES is still one word the shell passes as an
|
|
558
|
+
// argument (`grep 42 f`, `grep $PATTERN f`, `grep $(cmd) f`), so it spends
|
|
559
|
+
// a pattern positional even though no reliable operand text can be read
|
|
560
|
+
// from it. Counting only argument nodes left a numeric or computed pattern
|
|
561
|
+
// unseen, so the slot was spent on the command's real operand instead and
|
|
562
|
+
// the operand reached no path surface (#823).
|
|
563
|
+
//
|
|
564
|
+
// A redirect hosted on the command node is not an argument and is excluded;
|
|
565
|
+
// counting it would push the real pattern out as an operand token. The
|
|
566
|
+
// exclusion is the narrow side on purpose: miscounting an argument as a
|
|
567
|
+
// redirect drops an operand, while the reverse only over-surfaces.
|
|
568
|
+
if (!isArgNode) {
|
|
569
|
+
if (
|
|
570
|
+
!EXECUTION_HOST_TYPES.has(child.type) &&
|
|
571
|
+
!hasExplicitScript &&
|
|
572
|
+
positionalsSeen < patternPositionals
|
|
573
|
+
) {
|
|
574
|
+
positionalsSeen++;
|
|
575
|
+
}
|
|
576
|
+
// Recurse for nested commands (e.g. command_substitution).
|
|
577
|
+
tokens.push(...collectPathCandidateTokens(child));
|
|
435
578
|
continue;
|
|
436
579
|
}
|
|
437
580
|
|
|
@@ -447,11 +590,19 @@ function collectPatternCommandTokens(
|
|
|
447
590
|
case "end-of-flags":
|
|
448
591
|
pastEndOfFlags = true;
|
|
449
592
|
break;
|
|
450
|
-
case "consume-
|
|
451
|
-
|
|
452
|
-
if (directive.
|
|
593
|
+
case "consume-next":
|
|
594
|
+
pendingConsumption = directive.role;
|
|
595
|
+
if (suppliesScript(directive.role)) hasExplicitScript = true;
|
|
596
|
+
break;
|
|
597
|
+
case "inline-value":
|
|
598
|
+
if (directive.role === "script-file")
|
|
599
|
+
tokens.push({ token: directive.value, effect });
|
|
600
|
+
if (suppliesScript(directive.role)) hasExplicitScript = true;
|
|
453
601
|
break;
|
|
454
602
|
case "regular-flag":
|
|
603
|
+
// Unrecognized: fall back to the blind `--opt=value` split, which is
|
|
604
|
+
// safe precisely because the flag's role is unknown (#645).
|
|
605
|
+
tokens.push(...embeddedOptionValueToken(text, effect));
|
|
455
606
|
break;
|
|
456
607
|
}
|
|
457
608
|
continue;
|
|
@@ -459,17 +610,63 @@ function collectPatternCommandTokens(
|
|
|
459
610
|
|
|
460
611
|
// Positional argument.
|
|
461
612
|
if (!hasExplicitScript && positionalsSeen < patternPositionals) {
|
|
462
|
-
positionalsSeen++;
|
|
463
|
-
|
|
613
|
+
positionalsSeen++; // Skip: this is an inline pattern/script.
|
|
614
|
+
} else {
|
|
615
|
+
tokens.push({ token: text, effect });
|
|
464
616
|
}
|
|
465
|
-
|
|
466
|
-
//
|
|
467
|
-
tokens.push(
|
|
617
|
+
// A quoted flag never reaches the flag branch above, so its embedded value
|
|
618
|
+
// is split here instead.
|
|
619
|
+
tokens.push(...embeddedOptionValueToken(text, effect));
|
|
468
620
|
}
|
|
469
621
|
|
|
470
622
|
return tokens;
|
|
471
623
|
}
|
|
472
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Whether a flag in this role means the inline pattern positional is spent.
|
|
627
|
+
*
|
|
628
|
+
* `unknown-arity` says so for the opposite reason to the others: not because
|
|
629
|
+
* the script was supplied, but because the walker cannot tell which word the
|
|
630
|
+
* script is, and skipping the wrong one drops a real operand.
|
|
631
|
+
*/
|
|
632
|
+
function suppliesScript(role: PatternFlagRole): boolean {
|
|
633
|
+
return (
|
|
634
|
+
role === "script" || role === "script-file" || role === "unknown-arity"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* What a pending flag consumption made of the argument node that followed it.
|
|
640
|
+
*
|
|
641
|
+
* `consumed` is the flag's own verdict, not the walker's: a `suffix` flag
|
|
642
|
+
* declines a non-empty argument, which the walker then reads as an ordinary
|
|
643
|
+
* argument.
|
|
644
|
+
*/
|
|
645
|
+
interface ConsumptionDischarge {
|
|
646
|
+
readonly consumed: boolean;
|
|
647
|
+
/** The path candidate the consumed argument contributes, if any. */
|
|
648
|
+
readonly token?: PathToken;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Apply a pending consumption to the argument text that follows its flag. */
|
|
652
|
+
function dischargePendingConsumption(
|
|
653
|
+
role: PatternFlagRole,
|
|
654
|
+
text: string,
|
|
655
|
+
effect: TokenEffect,
|
|
656
|
+
): ConsumptionDischarge {
|
|
657
|
+
switch (role) {
|
|
658
|
+
case "script-file":
|
|
659
|
+
return { consumed: true, token: { token: text, effect } };
|
|
660
|
+
case "script":
|
|
661
|
+
case "value":
|
|
662
|
+
return { consumed: true };
|
|
663
|
+
case "suffix":
|
|
664
|
+
return { consumed: text === "" };
|
|
665
|
+
case "unknown-arity":
|
|
666
|
+
return { consumed: false };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
473
670
|
/**
|
|
474
671
|
* Collect all argument tokens from a generic (non-pattern-first) command node,
|
|
475
672
|
* skipping the command name and variable assignments.
|