@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19
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/dist/src/analysis.js +30 -72
- package/dist/src/augmentations.js +1 -1
- package/dist/src/calls.js +267 -48
- package/dist/src/commands/held.js +499 -0
- package/dist/src/commands/read.js +16 -10
- package/dist/src/commands/shell.js +109 -15
- package/dist/src/commands/tools.js +1540 -149
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/contract/contract.generated.js +9 -2
- package/dist/src/entries/conventions.js +7 -6
- package/dist/src/entries/test-runners.js +6 -40
- package/dist/src/entries/workflow-steps.js +202 -28
- package/dist/src/exempt.js +0 -26
- package/dist/src/exposure.js +400 -0
- package/dist/src/findings/completion.js +4 -17
- package/dist/src/findings/emitters.js +22 -33
- package/dist/src/findings/intra-function.js +3 -3
- package/dist/src/findings/non-code-artifacts.js +11 -5
- package/dist/src/findings/reads-and-writes.js +71 -15
- package/dist/src/findings/unused-declarations.js +2 -22
- package/dist/src/findings/visibility-narrowing.js +14 -20
- package/dist/src/inventory.js +18 -0
- package/dist/src/{components.js → liveness/components.js} +1 -1
- package/dist/src/{graph.js → liveness/graph.js} +38 -21
- package/dist/src/liveness/matrix.js +99 -0
- package/dist/src/liveness/sweep.js +558 -0
- package/dist/src/query.js +46 -0
- package/dist/src/references/sink.js +87 -1
- package/dist/src/references.js +1 -1
- package/dist/src/roots/manifest.js +47 -4
- package/dist/src/roots/published-reach.js +4 -4
- package/dist/src/roots/root-set.js +14 -16
- package/dist/src/text/named-files.js +257 -34
- package/dist/src/verbs/analyze.js +16 -1
- package/dist/src/verbs/explain.js +73 -152
- package/dist/src/verbs/print-retained.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +54 -108
- package/src/augmentations.ts +1 -1
- package/src/calls.ts +326 -52
- package/src/commands/held.ts +632 -0
- package/src/commands/read.ts +26 -12
- package/src/commands/shell.ts +117 -15
- package/src/commands/tools.ts +1972 -202
- package/src/conformance-record.ts +1 -1
- package/src/contract/contract.generated.ts +9 -2
- package/src/dependencies.ts +1 -1
- package/src/entries/conventions.ts +8 -12
- package/src/entries/test-runners.ts +12 -45
- package/src/entries/workflow-steps.ts +227 -38
- package/src/exempt.ts +31 -46
- package/src/exposure.ts +492 -0
- package/src/file-facts.ts +5 -0
- package/src/finding.ts +1 -1
- package/src/findings/boundary.ts +7 -0
- package/src/findings/completion.ts +5 -19
- package/src/findings/emitters.ts +23 -41
- package/src/findings/interfaces.ts +3 -3
- package/src/findings/intra-function.ts +3 -3
- package/src/findings/non-code-artifacts.ts +16 -5
- package/src/findings/reads-and-writes.ts +99 -26
- package/src/findings/unused-declarations.ts +4 -28
- package/src/findings/visibility-narrowing.ts +25 -26
- package/src/interface-satisfaction.ts +1 -1
- package/src/inventory.ts +19 -0
- package/src/{components.ts → liveness/components.ts} +3 -3
- package/src/{graph.ts → liveness/graph.ts} +74 -26
- package/src/liveness/matrix.ts +162 -0
- package/src/liveness/sweep.ts +855 -0
- package/src/query.ts +81 -0
- package/src/references/sink.ts +101 -0
- package/src/references.ts +8 -1
- package/src/roots/manifest.ts +53 -8
- package/src/roots/published-reach.ts +9 -4
- package/src/roots/root-set.ts +18 -16
- package/src/stores.ts +2 -1
- package/src/tests/file-class.ts +19 -2
- package/src/text/named-files.ts +333 -36
- package/src/verbs/analyze.ts +16 -1
- package/src/verbs/explain.ts +81 -199
- package/src/verbs/print-retained.ts +1 -1
- package/dist/src/matrix.js +0 -275
- package/dist/src/sweep.js +0 -234
- package/src/matrix.ts +0 -466
- package/src/sweep.ts +0 -384
|
@@ -3,48 +3,130 @@
|
|
|
3
3
|
* its program word and the options it names its configuration file by, each as the tool
|
|
4
4
|
* documents them, and one reader per shell builtin that moves the shell's directory. A reader
|
|
5
5
|
* never guesses: an option it does not handle, or a dynamic word where it needs a value, leaves
|
|
6
|
-
* the command unread.
|
|
6
|
+
* the command unread. The same rows and readers decide, for layer 3, which words of a command
|
|
7
|
+
* may write the file a runtime runs, so the two layers never read one command apart.
|
|
7
8
|
*/
|
|
8
9
|
import { hasURLScheme, isPathSpecifier, resolvePath } from "../paths.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
import { PART } from "./shell.js";
|
|
11
|
+
const UNKNOWN = { kind: "unknown" };
|
|
12
|
+
function isUnknown(value) {
|
|
13
|
+
return value === UNKNOWN;
|
|
14
|
+
}
|
|
15
|
+
/** The text `word` writes before its first part only the run knows: all of it where it is static. */
|
|
16
|
+
function startOf(word) {
|
|
17
|
+
return word.value ?? word.written?.split(PART)[0] ?? "";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The options one word `word`, starting with `-`, writes, the word at `at`, or `undefined` where
|
|
21
|
+
* `options` does not hold one. A valued one's value is the next word's unless the word writes
|
|
22
|
+
* it, and always where it is `dynamic`, whose written text goes on past `word`.
|
|
23
|
+
*/
|
|
24
|
+
function optionsInWord(word, at, options, dynamic) {
|
|
25
|
+
const inWord = (name, prefix) => ({ name, value: { at, prefix } });
|
|
26
|
+
if (word.startsWith("--")) {
|
|
27
|
+
const equals = word.indexOf("=");
|
|
28
|
+
const name = equals < 0 ? word : word.slice(0, equals);
|
|
29
|
+
if (equals >= 0) {
|
|
30
|
+
return options.valued.has(name) || options.optional?.has(name) === true
|
|
31
|
+
? [inWord(name, `${name}=`)]
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
if (options.optional?.has(word) === true) {
|
|
35
|
+
return [inWord(word, word)];
|
|
36
|
+
}
|
|
37
|
+
return options.valued.has(word) && !dynamic
|
|
38
|
+
? [{ name: word, value: { at: at + 1, prefix: "" } }]
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
if (!dynamic && options.valued.has(word)) {
|
|
42
|
+
return [{ name: word, value: { at: at + 1, prefix: "" } }];
|
|
43
|
+
}
|
|
44
|
+
if (options.attached !== true) {
|
|
45
|
+
return options.optional?.has(word) === true ? [inWord(word, word)] : undefined;
|
|
46
|
+
}
|
|
47
|
+
const found = [];
|
|
48
|
+
for (let letter = 1; letter < word.length; letter += 1) {
|
|
49
|
+
const name = `-${word.charAt(letter)}`;
|
|
50
|
+
const prefix = word.slice(0, letter + 1);
|
|
51
|
+
if (options.valued.has(name)) {
|
|
52
|
+
const next = letter + 1 === word.length && !dynamic;
|
|
53
|
+
return [...found, next ? { name, value: { at: at + 1, prefix: "" } } : inWord(name, prefix)];
|
|
54
|
+
}
|
|
55
|
+
if (options.optional?.has(name) === true) {
|
|
56
|
+
return [...found, inWord(name, prefix)];
|
|
57
|
+
}
|
|
58
|
+
if (!options.flags.has(name)) {
|
|
18
59
|
return undefined;
|
|
19
60
|
}
|
|
20
|
-
|
|
21
|
-
|
|
61
|
+
found.push({ name });
|
|
62
|
+
}
|
|
63
|
+
return found.length === 0 || dynamic ? undefined : found;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The options `words` write from `from` on, read by `options`, the one reading of a table's
|
|
67
|
+
* options that {@link optionsOf}, {@link optionLines} and {@link envSplit} share. A dynamic
|
|
68
|
+
* word that starts with written text other than `-` is no option; one whose written start is a
|
|
69
|
+
* valued option's ends them with that option's value in it, as `-S"$CMD"` does.
|
|
70
|
+
*/
|
|
71
|
+
function scanOptions(words, options, from = 0) {
|
|
72
|
+
const found = [];
|
|
73
|
+
for (let at = from; at < words.length; at += 1) {
|
|
74
|
+
const one = words[at];
|
|
75
|
+
const dynamic = one?.value === undefined;
|
|
76
|
+
const word = one === undefined ? "" : startOf(one);
|
|
77
|
+
if (!dynamic && word === "--") {
|
|
78
|
+
return { options: found, at: at + 1, stop: "ended" };
|
|
22
79
|
}
|
|
23
|
-
if (!
|
|
24
|
-
|
|
25
|
-
return { values, at, ended: false };
|
|
26
|
-
}
|
|
80
|
+
if (!dynamic && options.flags.has(word)) {
|
|
81
|
+
found.push({ name: word });
|
|
27
82
|
continue;
|
|
28
83
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (options.valued.has(name)) {
|
|
32
|
-
const value = equals < 0 ? words[(at += 1)]?.value : word.slice(equals + 1);
|
|
33
|
-
if (value === undefined) {
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
keep(name, value);
|
|
84
|
+
if (!word.startsWith("-")) {
|
|
85
|
+
return { options: found, at, stop: dynamic && word === "" ? "dynamic" : "operand" };
|
|
37
86
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
const read = optionsInWord(word, at, options, dynamic);
|
|
88
|
+
if (dynamic || read === undefined) {
|
|
89
|
+
return {
|
|
90
|
+
options: [...found, ...(read ?? [])],
|
|
91
|
+
at,
|
|
92
|
+
stop: dynamic ? "dynamic" : "unread",
|
|
93
|
+
};
|
|
42
94
|
}
|
|
43
|
-
|
|
95
|
+
found.push(...read);
|
|
96
|
+
at = read.at(-1)?.value?.at ?? at;
|
|
97
|
+
}
|
|
98
|
+
return { options: found, at: words.length, stop: "end" };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The options `words` start with, {@link UNKNOWN} where `options` does not hold one, or
|
|
102
|
+
* `undefined` where a needed word is dynamic.
|
|
103
|
+
*/
|
|
104
|
+
function optionsOf(words, options) {
|
|
105
|
+
const scan = scanOptions(words, options);
|
|
106
|
+
if (scan.stop === "unread") {
|
|
107
|
+
return UNKNOWN;
|
|
108
|
+
}
|
|
109
|
+
if (scan.stop === "dynamic") {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const values = new Map();
|
|
113
|
+
const named = new Set();
|
|
114
|
+
for (const { name, value } of scan.options) {
|
|
115
|
+
named.add(name);
|
|
116
|
+
const text = value === undefined ? undefined : words[value.at]?.value;
|
|
117
|
+
if (value !== undefined && text === undefined) {
|
|
44
118
|
return undefined;
|
|
45
119
|
}
|
|
120
|
+
if (value !== undefined && text !== undefined) {
|
|
121
|
+
values.set(name, [...(values.get(name) ?? []), text.slice(value.prefix.length)]);
|
|
122
|
+
}
|
|
46
123
|
}
|
|
47
|
-
return { values, at:
|
|
124
|
+
return { values, named, at: scan.at, ended: scan.stop === "ended" };
|
|
125
|
+
}
|
|
126
|
+
/** The place of the first operand after the options `words` start with, `undefined` where {@link optionsOf} reads none. */
|
|
127
|
+
function operandAt(words, options) {
|
|
128
|
+
const read = optionsOf(words, options);
|
|
129
|
+
return read === undefined || isUnknown(read) ? undefined : read.at;
|
|
48
130
|
}
|
|
49
131
|
/**
|
|
50
132
|
* The path a URL's path text names, percent-decoded as Node's `fileURLToPath` does, with a
|
|
@@ -90,57 +172,189 @@ export function preloadTarget(preload, dir) {
|
|
|
90
172
|
const path = urlPath(rest);
|
|
91
173
|
return path === undefined ? undefined : { path: resolvePath(dir, path) };
|
|
92
174
|
}
|
|
93
|
-
/**
|
|
94
|
-
function
|
|
95
|
-
|
|
175
|
+
/** The options of a runtime's command line, as {@link optionsOf} reads them. */
|
|
176
|
+
function runtimeOptions(line) {
|
|
177
|
+
return {
|
|
178
|
+
valued: new Set([...line.preloads.keys(), ...(line.inline ?? []), ...(line.valued ?? [])]),
|
|
179
|
+
flags: new Set([...line.flags, ...(line.stdin === true ? ["-"] : [])]),
|
|
180
|
+
optional: new Set(line.optional),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** The entries that name the runtime's own standard input: https://man7.org/linux/man-pages/man4/fd.4.html */
|
|
184
|
+
const STDIN_PATHS = new Set(["/dev/stdin", "/dev/fd/0"]);
|
|
185
|
+
/**
|
|
186
|
+
* How a runtime reads its words, those after its program word, {@link UNKNOWN} where one of
|
|
187
|
+
* its options is unhandled, or `undefined` where a word it needs is dynamic: the one reading
|
|
188
|
+
* layer 2 runs a runtime by and layer 3 finds the word that may write its entry by.
|
|
189
|
+
*/
|
|
190
|
+
function runtimeLine(line, words) {
|
|
191
|
+
const options = runtimeOptions(line);
|
|
192
|
+
const before = optionsOf(words, options);
|
|
193
|
+
if (before === undefined || isUnknown(before)) {
|
|
194
|
+
return before;
|
|
195
|
+
}
|
|
196
|
+
const picked = words[before.at]?.value;
|
|
197
|
+
const modal = line.modes?.has(picked ?? "") === true &&
|
|
198
|
+
words
|
|
199
|
+
.slice(0, before.at)
|
|
200
|
+
.every((word) => line.modeAfter === undefined || line.modeAfter.has(word.value ?? ""));
|
|
201
|
+
const from = picked !== undefined && line.subcommands?.has(picked) === true ? before.at + 1 : 0;
|
|
202
|
+
const read = from === 0 ? before : optionsOf(words.slice(from), options);
|
|
203
|
+
if (read === undefined || isUnknown(read)) {
|
|
204
|
+
return read;
|
|
205
|
+
}
|
|
206
|
+
const values = (name) => [
|
|
207
|
+
...(from === 0 ? [] : (before.values.get(name) ?? [])),
|
|
208
|
+
...(read.values.get(name) ?? []),
|
|
209
|
+
];
|
|
210
|
+
const named = new Set([...(from === 0 ? [] : before.named), ...read.named]);
|
|
211
|
+
const runner = (line.runner ?? []).some((name) => named.has(name));
|
|
212
|
+
const inline = runner || (line.inline ?? []).some((name) => values(name).length > 0);
|
|
213
|
+
// `-` runs its standard input, and hands every later word to it.
|
|
214
|
+
const dash = words.slice(0, from + read.at).some((word) => word.value === "-");
|
|
215
|
+
const entry = dash ? words.length : from + read.at;
|
|
216
|
+
const piped = STDIN_PATHS.has(words[entry]?.value ?? "");
|
|
217
|
+
return {
|
|
218
|
+
entry: inline || piped || entry >= words.length || (from === 0 && modal) ? undefined : entry,
|
|
219
|
+
stdin: !inline && (piped || (line.stdin === true && entry >= words.length)),
|
|
220
|
+
tests: runner ? from + read.at : undefined,
|
|
221
|
+
named,
|
|
222
|
+
preloads: [...line.preloads].flatMap(([name, resolution]) => values(name).map((specifier) => ({ specifier, resolution }))),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/** The reader of a runtime's command line. */
|
|
226
|
+
function runtime(line) {
|
|
96
227
|
return (words) => {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return undefined;
|
|
228
|
+
const read = runtimeLine(line, words);
|
|
229
|
+
if (isUnknown(read)) {
|
|
230
|
+
return UNKNOWN;
|
|
101
231
|
}
|
|
102
|
-
const entry = words[
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
232
|
+
const entry = read?.entry === undefined ? undefined : words[read.entry];
|
|
233
|
+
if (read === undefined ||
|
|
234
|
+
(read.stdin && words.some((word) => word.value === "-")) ||
|
|
235
|
+
(entry !== undefined && entry.value === undefined) ||
|
|
236
|
+
read.preloads.some((one) => preloadTarget(one, "/") === undefined)) {
|
|
106
237
|
return undefined;
|
|
107
238
|
}
|
|
108
239
|
return {
|
|
109
240
|
kind: "run",
|
|
110
241
|
entry: entry?.value,
|
|
111
|
-
preloads:
|
|
112
|
-
runtime: runsNode && entry !== undefined ? "node" : undefined,
|
|
242
|
+
preloads: read.preloads,
|
|
243
|
+
runtime: line.runsNode && entry !== undefined ? "node" : undefined,
|
|
113
244
|
};
|
|
114
245
|
};
|
|
115
246
|
}
|
|
247
|
+
/** The options that run their value, code or a script's name, in a runtime's entry's place. */
|
|
248
|
+
const INLINE = ["-e", "--eval", "-p", "--print"];
|
|
116
249
|
/**
|
|
117
250
|
* `node [options] [entry] [arguments]`, https://nodejs.org/api/cli.html: the options that load
|
|
118
|
-
* a module before the entry, `-r` as `require` does and the rest as an ECMAScript module
|
|
119
|
-
*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
[
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
["
|
|
133
|
-
|
|
251
|
+
* a module before the entry, `-r` as `require` does and the rest as an ECMAScript module;
|
|
252
|
+
* `--run`, which runs a script of the manifest; and the options that leave the entry where it
|
|
253
|
+
* is, `--inspect` and its kin and V8's taking their value only after `=`. With no entry, or
|
|
254
|
+
* `-`, it runs its standard input, which the second layer does not read, but where `--test`
|
|
255
|
+
* runs its test runner on the files its operands, or its default patterns, name.
|
|
256
|
+
*/
|
|
257
|
+
const NODE = {
|
|
258
|
+
preloads: new Map([
|
|
259
|
+
["-r", "require"],
|
|
260
|
+
["--require", "require"],
|
|
261
|
+
["--import", "import"],
|
|
262
|
+
["--loader", "import"],
|
|
263
|
+
["--experimental-loader", "import"],
|
|
264
|
+
]),
|
|
265
|
+
flags: long(...["no-warnings", "no-deprecation", "enable-source-maps", "experimental-vm-modules"], ...["experimental-strip-types", "no-experimental-strip-types", "experimental-transform-types"], ...["experimental-detect-module", "no-experimental-detect-module"], ...["experimental-require-module", "no-experimental-require-module", "trace-warnings"], ...["trace-deprecation", "trace-uncaught", "throw-deprecation", "pending-deprecation"], ...["expose-gc", "preserve-symlinks", "preserve-symlinks-main", "frozen-intrinsics"], ...["abort-on-uncaught-exception", "watch", "watch-preserve-output", "jitless"], ...["openssl-legacy-provider", "use-openssl-ca", "use-system-ca", "use-bundled-ca"], ...["no-addons", "insecure-http-parser", "version", "help", "v8-options", "test"], ...["test-only", "test-force-exit", "test-update-snapshots", "test-randomize"], ...["experimental-test-coverage", "experimental-test-module-mocks", "no-strip-types"], ...["experimental-test-snapshots"]).concat(["-v", "-h"]),
|
|
266
|
+
valued: [
|
|
267
|
+
"-C",
|
|
268
|
+
...long("conditions", "input-type", "env-file", "env-file-if-exists", "title"),
|
|
269
|
+
...long("disable-warning", "unhandled-rejections", "watch-path", "dns-result-order"),
|
|
270
|
+
...long("icu-data-dir", "redirect-warnings", "diagnostic-dir", "max-http-header-size"),
|
|
271
|
+
...long("test-reporter", "test-reporter-destination", "test-concurrency", "test-timeout"),
|
|
272
|
+
...long("test-name-pattern", "test-skip-pattern", "test-shard", "test-isolation"),
|
|
273
|
+
...long("test-coverage-include", "test-coverage-exclude", "test-coverage-branches"),
|
|
274
|
+
...long("test-coverage-functions", "test-coverage-lines", "test-global-setup"),
|
|
275
|
+
],
|
|
276
|
+
optional: long("inspect", "inspect-brk", "inspect-wait", "max-old-space-size", "stack-size"),
|
|
277
|
+
runsNode: true,
|
|
278
|
+
inline: [...INLINE, "--run"],
|
|
279
|
+
stdin: true,
|
|
280
|
+
runner: ["--test"],
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* `tsx [watch] [options] <file> [arguments]`, https://tsx.is/usage, which takes Node's options
|
|
284
|
+
* too: https://tsx.is/node-enhancement
|
|
285
|
+
*/
|
|
286
|
+
const TSX = {
|
|
287
|
+
preloads: new Map([
|
|
288
|
+
["-r", "require"],
|
|
289
|
+
["--require", "require"],
|
|
290
|
+
["--import", "import"],
|
|
291
|
+
]),
|
|
292
|
+
// `tsx --test` runs Node's test runner on files only `node --test` is read for, so it caps.
|
|
293
|
+
flags: [...NODE.flags.filter((name) => name !== "--test"), "--no-cache"],
|
|
294
|
+
valued: [...(NODE.valued ?? []), "--tsconfig"],
|
|
295
|
+
optional: NODE.optional ?? [],
|
|
296
|
+
runsNode: false,
|
|
297
|
+
subcommands: new Set(["watch"]),
|
|
298
|
+
stdin: true,
|
|
299
|
+
};
|
|
134
300
|
/** `ts-node [options] [script] [arguments]`, https://typestrong.org/ts-node/docs/options; `--esm` picks no file. */
|
|
135
|
-
const
|
|
136
|
-
[
|
|
137
|
-
|
|
138
|
-
|
|
301
|
+
const TS_NODE = {
|
|
302
|
+
preloads: new Map([
|
|
303
|
+
["-r", "require"],
|
|
304
|
+
["--require", "require"],
|
|
305
|
+
]),
|
|
306
|
+
flags: ["--esm", "-v", "--version", "-h", "--help"],
|
|
307
|
+
runsNode: false,
|
|
308
|
+
inline: INLINE,
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* `bun [options] [run] [options] <file> [arguments]`, https://bun.com/docs/cli/run; its package
|
|
312
|
+
* manager, test runner, bundler and `bun x` subcommands run no entry.
|
|
313
|
+
*/
|
|
314
|
+
const BUN = {
|
|
315
|
+
preloads: new Map([
|
|
316
|
+
["-r", "require"],
|
|
317
|
+
["--preload", "require"],
|
|
318
|
+
]),
|
|
319
|
+
flags: [
|
|
320
|
+
"--watch",
|
|
321
|
+
"--hot",
|
|
322
|
+
"--smol",
|
|
323
|
+
"--bun",
|
|
324
|
+
"--silent",
|
|
325
|
+
"--no-install",
|
|
326
|
+
"-v",
|
|
327
|
+
"--version",
|
|
328
|
+
].concat(["-h", "--help"]),
|
|
329
|
+
runsNode: false,
|
|
330
|
+
subcommands: new Set(["run"]),
|
|
331
|
+
modes: new Set(["install", "i", "add", "a", "remove", "rm", "update", "outdated", "link", "unlink", "pm"]
|
|
332
|
+
.concat(["test", "build", "init", "create", "c", "upgrade", "publish", "patch", "audit"])
|
|
333
|
+
.concat(["info", "repl", "exec", "x"])),
|
|
334
|
+
inline: INLINE,
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* `deno [run|serve] [options] <file> [arguments]`, https://docs.deno.com/runtime/reference/cli/run/
|
|
338
|
+
* and https://docs.deno.com/runtime/reference/cli/serve/, with the permission flags written
|
|
339
|
+
* whole; its other subcommands run no entry: https://docs.deno.com/runtime/reference/cli/,
|
|
340
|
+
* found past its global options only, so `deno -A lint` runs `lint`:
|
|
341
|
+
* https://github.com/denoland/deno/blob/main/libs/cli_parser/src/parse.rs
|
|
342
|
+
*/
|
|
343
|
+
const DENO = {
|
|
344
|
+
preloads: new Map(),
|
|
345
|
+
flags: ["-A", "--allow-all", "-q", "--quiet", "--watch", "--no-check", "-V", "--version"].concat(["-h", "--help"], ["read", "write", "net", "env", "run", "sys", "ffi", "import"].map((name) => `--allow-${name}`)),
|
|
346
|
+
runsNode: false,
|
|
347
|
+
subcommands: new Set(["run", "serve"]),
|
|
348
|
+
modeAfter: new Set(["-q", "--quiet"]),
|
|
349
|
+
modes: new Set(["task", "test", "fmt", "lint", "compile", "install", "uninstall", "eval", "repl", "bench"]
|
|
350
|
+
.concat(["check", "doc", "info", "init", "upgrade", "cache", "coverage", "types", "add"])
|
|
351
|
+
.concat(["remove", "outdated", "publish", "jupyter", "clean", "completions", "help"])),
|
|
352
|
+
};
|
|
139
353
|
/** Hands on the command `words` from `at`, after the assignments `assigns` accepts. */
|
|
140
354
|
function afterAssignments(words, at, assigns) {
|
|
141
355
|
let end = at;
|
|
142
|
-
|
|
143
|
-
if (!assigns(
|
|
356
|
+
for (let word = words[end]; word !== undefined && startOf(word).includes("="); word = words[end]) {
|
|
357
|
+
if (!assigns(startOf(word))) {
|
|
144
358
|
return undefined;
|
|
145
359
|
}
|
|
146
360
|
end += 1;
|
|
@@ -151,60 +365,127 @@ function afterAssignments(words, at, assigns) {
|
|
|
151
365
|
}
|
|
152
366
|
return program === undefined
|
|
153
367
|
? { kind: "run", entry: undefined, preloads: [], runtime: undefined }
|
|
154
|
-
: { kind: "command", words: words.slice(end) };
|
|
368
|
+
: { kind: "command", words: words.slice(end), at: end };
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* GNU env's options, https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html,
|
|
372
|
+
* whose `-S` and `--split-string` give a command line:
|
|
373
|
+
* https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html#g_t_002dS_002f_002d_002dsplit_002dstring-usage-in-scripts
|
|
374
|
+
*/
|
|
375
|
+
const ENV = {
|
|
376
|
+
valued: new Set(["-u", "--unset", "-S", "--split-string", "-C", "--chdir"]),
|
|
377
|
+
flags: new Set([
|
|
378
|
+
...["-i", "--ignore-environment", "-", "-v", "--debug", "-0", "--null"],
|
|
379
|
+
"--list-signal-handling",
|
|
380
|
+
]),
|
|
381
|
+
attached: true,
|
|
382
|
+
optional: new Set(long("default-signal", "ignore-signal", "block-signal")),
|
|
383
|
+
};
|
|
384
|
+
/** env's options that give a command line, and the one that moves it to another directory. */
|
|
385
|
+
const SPLIT = new Set(["-S", "--split-string"]);
|
|
386
|
+
const CHDIR = new Set(["-C", "--chdir"]);
|
|
387
|
+
/**
|
|
388
|
+
* The command line env runs where a `-S` or `--split-string` stands: GNU env splits its value
|
|
389
|
+
* on blanks, puts those words in place of the option and its value, before env's later words,
|
|
390
|
+
* and reads its options again from the first of them, parse_split_string of
|
|
391
|
+
* https://github.com/coreutils/coreutils/blob/master/src/env.c. {@link UNKNOWN} where the value
|
|
392
|
+
* is dynamic, writes a quote, an escape, a `$` or a `#`, which env reads otherwise, or follows
|
|
393
|
+
* a `-C`; `undefined` where none stands.
|
|
394
|
+
*/
|
|
395
|
+
function envSplit(words) {
|
|
396
|
+
const { options } = scanOptions(words, ENV);
|
|
397
|
+
const split = options.findIndex(({ name }) => SPLIT.has(name));
|
|
398
|
+
const value = options[split]?.value;
|
|
399
|
+
if (value === undefined) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
const text = words[value.at]?.value?.slice(value.prefix.length);
|
|
403
|
+
if (text === undefined ||
|
|
404
|
+
/['"\\$#]/u.test(text) ||
|
|
405
|
+
options.slice(0, split).some(({ name }) => CHDIR.has(name))) {
|
|
406
|
+
return UNKNOWN;
|
|
407
|
+
}
|
|
408
|
+
const spliced = text.split(/\s+/u).filter((one) => one !== "");
|
|
409
|
+
const later = words.slice(value.at + 1).map((word) => word.text);
|
|
410
|
+
return ["env", ...spliced.map((one) => `'${one}'`), ...later].join(" ");
|
|
155
411
|
}
|
|
156
412
|
/**
|
|
157
|
-
* `env [option]... [name=value]... [command [argument]...]
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
413
|
+
* `env [option]... [name=value]... [command [argument]...]`: the options that change only the
|
|
414
|
+
* environment, `-i`, `-u` and `-v` in each spelling, and `-S`, read by {@link envSplit}. GNU env
|
|
415
|
+
* takes any word holding an `=` after its first character as an assignment, and fails before
|
|
416
|
+
* running anything on a `-u` name that is empty or holds an `=`. A dynamic word whose written
|
|
417
|
+
* start is an assignment's is one, as env reads the text it expands to.
|
|
162
418
|
*/
|
|
163
419
|
const env = (words) => {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
420
|
+
const split = envSplit(words);
|
|
421
|
+
if (split !== undefined) {
|
|
422
|
+
return typeof split === "string" ? { kind: "line", text: split } : split;
|
|
423
|
+
}
|
|
424
|
+
const read = optionsOf(words, ENV);
|
|
425
|
+
if (read === undefined || isUnknown(read) || [...CHDIR].some((name) => read.named.has(name))) {
|
|
426
|
+
return read === undefined ? undefined : UNKNOWN;
|
|
427
|
+
}
|
|
428
|
+
const unset = [...(read.values.get("-u") ?? []), ...(read.values.get("--unset") ?? [])];
|
|
429
|
+
return unset.some((name) => name === "" || name.includes("="))
|
|
171
430
|
? undefined
|
|
172
431
|
: afterAssignments(words, read.at, (value) => !value.startsWith("="));
|
|
173
432
|
};
|
|
433
|
+
/** The command line env runs where a `-S` stands, as {@link envSplit} reads it. */
|
|
434
|
+
function envLines(words) {
|
|
435
|
+
const split = envSplit(words);
|
|
436
|
+
return typeof split === "string" ? { ...NO_LINES, texts: [split] } : NO_LINES;
|
|
437
|
+
}
|
|
174
438
|
/** `cross-env [name=value]... command [argument]...`, https://github.com/kentcdodds/cross-env#usage */
|
|
175
439
|
const crossEnv = (words) => {
|
|
176
440
|
const reading = afterAssignments(words, 0, (value) => /^\w+=/u.test(value));
|
|
177
441
|
return reading?.kind === "command" ? reading : undefined;
|
|
178
442
|
};
|
|
443
|
+
/** The command a package runner's operand names: a package specifier's, whose version picks no other command. */
|
|
444
|
+
function commandName(operand, packaged) {
|
|
445
|
+
return packaged ? operand.replace(/(?<=.)@[^/@]*$/u, "") : operand;
|
|
446
|
+
}
|
|
179
447
|
/** The command a package runner hands on: its first word a package's or a command's name. */
|
|
180
448
|
function runnerCommand(words, packaged) {
|
|
181
449
|
const [first, ...rest] = words;
|
|
182
450
|
if (first?.value === undefined || first.value.startsWith("-")) {
|
|
183
451
|
return undefined;
|
|
184
452
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
453
|
+
const name = commandName(first.value, packaged);
|
|
454
|
+
return { kind: "command", words: [{ value: name, text: first.text }, ...rest], at: 0 };
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* npx's and npm exec's options, https://docs.npmjs.com/cli/v11/commands/npx and
|
|
458
|
+
* https://docs.npmjs.com/cli/v11/commands/npm-exec: `--package` (and npx's `-p`), `-c` and
|
|
459
|
+
* `--call`, which give a command line, `--yes` and `--no`.
|
|
460
|
+
*/
|
|
461
|
+
function packageExecOptions(shortPackage) {
|
|
462
|
+
return {
|
|
463
|
+
valued: new Set(["--package", "-c", "--call", ...(shortPackage ? ["-p"] : [])]),
|
|
464
|
+
flags: new Set(["-y", "--yes", "--no"]),
|
|
465
|
+
};
|
|
188
466
|
}
|
|
467
|
+
const NPX = packageExecOptions(true);
|
|
468
|
+
const NPM_EXEC = packageExecOptions(false);
|
|
469
|
+
const CALL = new Set(["-c", "--call"]);
|
|
189
470
|
/**
|
|
190
|
-
* npx and npm exec,
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* runs unless a `--package` names one. npm exec reads options after it too, unless a `--` ends them.
|
|
471
|
+
* npx and npm exec, by `options`, before the first positional word, which is the package whose
|
|
472
|
+
* command runs unless a `--package` names one. npm exec reads options after it too, unless a
|
|
473
|
+
* `--` ends them.
|
|
194
474
|
*/
|
|
195
|
-
function packageExec(
|
|
196
|
-
const valued = new Set(["--package", "-c", "--call", ...(shortPackage ? ["-p"] : [])]);
|
|
475
|
+
function packageExec(options, laterOptions) {
|
|
197
476
|
return (words) => {
|
|
198
|
-
const read = optionsOf(words,
|
|
199
|
-
if (read === undefined) {
|
|
200
|
-
return
|
|
477
|
+
const read = optionsOf(words, options);
|
|
478
|
+
if (read === undefined || isUnknown(read)) {
|
|
479
|
+
return read;
|
|
201
480
|
}
|
|
202
481
|
const operands = words.slice(read.at);
|
|
203
|
-
if (laterOptions &&
|
|
204
|
-
!read.ended &&
|
|
205
|
-
operands.some((word) => word.value?.startsWith("-") !== false)) {
|
|
482
|
+
if (laterOptions && !read.ended && operands.some((word) => word.value === undefined)) {
|
|
206
483
|
return undefined;
|
|
207
484
|
}
|
|
485
|
+
// npm exec reads an option after its command as its own, whose table no row places.
|
|
486
|
+
if (laterOptions && !read.ended && operands.some((word) => word.value?.startsWith("-"))) {
|
|
487
|
+
return UNKNOWN;
|
|
488
|
+
}
|
|
208
489
|
const call = [...(read.values.get("-c") ?? []), ...(read.values.get("--call") ?? [])];
|
|
209
490
|
if (call.length > 0) {
|
|
210
491
|
return call.length === 1 && operands.length === 0 && call[0] !== undefined
|
|
@@ -212,11 +493,11 @@ function packageExec(shortPackage, laterOptions) {
|
|
|
212
493
|
: undefined;
|
|
213
494
|
}
|
|
214
495
|
const packages = [...(read.values.get("--package") ?? []), ...(read.values.get("-p") ?? [])];
|
|
215
|
-
return runnerCommand(operands, packages.length === 0);
|
|
496
|
+
return after(read.at, runnerCommand(operands, packages.length === 0));
|
|
216
497
|
};
|
|
217
498
|
}
|
|
218
|
-
const npx = packageExec(
|
|
219
|
-
const npmExec = packageExec(
|
|
499
|
+
const npx = packageExec(NPX, false);
|
|
500
|
+
const npmExec = packageExec(NPM_EXEC, true);
|
|
220
501
|
/**
|
|
221
502
|
* `npm run <script> [-- <arguments>]`, https://docs.npmjs.com/cli/v11/commands/npm-run-script:
|
|
222
503
|
* it runs another script of the same manifest, which is read on its own.
|
|
@@ -232,76 +513,695 @@ const npmRun = (words) => {
|
|
|
232
513
|
}
|
|
233
514
|
return { kind: "run", entry: undefined, preloads: [], runtime: undefined };
|
|
234
515
|
};
|
|
235
|
-
/**
|
|
236
|
-
function
|
|
237
|
-
return
|
|
516
|
+
/** `reading`, of the words after `skipped` others, as a reading of those words and them. */
|
|
517
|
+
function after(skipped, reading) {
|
|
518
|
+
return reading?.kind === "command" ? { ...reading, at: reading.at + skipped } : reading;
|
|
238
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* A tool whose second word picks the reader of the rest. Where its own options `options` stand
|
|
522
|
+
* before that word, it is not read, as they may run the command in another directory, and
|
|
523
|
+
* {@link UNKNOWN} where `options` does not hold one.
|
|
524
|
+
*/
|
|
525
|
+
function subcommands(readers, options) {
|
|
526
|
+
return (words) => {
|
|
527
|
+
const [first, ...rest] = words;
|
|
528
|
+
if (options !== undefined && first?.value?.startsWith("-") === true) {
|
|
529
|
+
return isUnknown(optionsOf(words, options)) ? UNKNOWN : undefined;
|
|
530
|
+
}
|
|
531
|
+
return first?.value === undefined ? undefined : after(1, readers.get(first.value)?.(rest));
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/** npm's own options before its subcommand: https://docs.npmjs.com/cli/v11/using-npm/config */
|
|
535
|
+
const NPM = {
|
|
536
|
+
valued: new Set(["-w", ...long("workspace", "prefix", "loglevel")]),
|
|
537
|
+
flags: new Set([
|
|
538
|
+
...["-ws", ...letters("sqdy")],
|
|
539
|
+
...long("workspaces", "no-workspaces", "include-workspace-root", "if-present", "silent"),
|
|
540
|
+
...long("quiet", "verbose", "yes", "no"),
|
|
541
|
+
...["-v", "-h", ...long("version", "help")],
|
|
542
|
+
]),
|
|
543
|
+
};
|
|
239
544
|
/** `npm exec`, `npm x` and `npm run` with `run-script`. */
|
|
240
545
|
const npm = subcommands(new Map([
|
|
241
546
|
["exec", npmExec],
|
|
242
547
|
["x", npmExec],
|
|
243
548
|
["run", npmRun],
|
|
244
549
|
["run-script", npmRun],
|
|
245
|
-
]));
|
|
550
|
+
]), NPM);
|
|
551
|
+
/** `yarn dlx`'s options: https://yarnpkg.com/cli/dlx */
|
|
552
|
+
const YARN_DLX = {
|
|
553
|
+
valued: new Set(["-p", "--package"]),
|
|
554
|
+
flags: new Set(["-q", "--quiet"]),
|
|
555
|
+
};
|
|
556
|
+
/** `bunx`'s options: https://bun.com/docs/pm/bunx */
|
|
557
|
+
const BUNX = { valued: new Set(["-p", "--package"]), flags: new Set() };
|
|
558
|
+
/**
|
|
559
|
+
* pnpm's own options, which it reads before or after its subcommand, `exec`'s `-c` among them:
|
|
560
|
+
* https://pnpm.io/pnpm-cli#options, https://pnpm.io/filtering, https://pnpm.io/cli/recursive,
|
|
561
|
+
* https://pnpm.io/cli/exec
|
|
562
|
+
*/
|
|
563
|
+
const PNPM = {
|
|
564
|
+
valued: new Set([
|
|
565
|
+
...["-C", "-F", ...long("dir", "filter", "filter-prod", "test-pattern", "resume-from")],
|
|
566
|
+
...long("changed-files-ignore-pattern", "workspace-concurrency", "reporter", "loglevel"),
|
|
567
|
+
]),
|
|
568
|
+
flags: new Set([
|
|
569
|
+
...letters("wrscvh"),
|
|
570
|
+
...long("version", "help"),
|
|
571
|
+
...long("workspace-root", "recursive", "parallel", "stream", "aggregate-output", "bail"),
|
|
572
|
+
...long("no-bail", "sort", "no-sort", "reverse", "report-summary", "fail-if-no-match"),
|
|
573
|
+
...long("silent", "color", "no-color", "shell-mode"),
|
|
574
|
+
]),
|
|
575
|
+
attached: true,
|
|
576
|
+
};
|
|
577
|
+
/** The options of `pnpm exec` and `pnpm dlx` that run their operands in a shell: https://pnpm.io/cli/exec, https://pnpm.io/cli/dlx */
|
|
578
|
+
const PNPM_LINES = new Set(["-c", "--shell-mode"]);
|
|
579
|
+
/** pnpm's options after `dlx`, its own and `--package`. */
|
|
580
|
+
const PNPM_DLX_OPTIONS = { ...PNPM, valued: new Set([...PNPM.valued, "--package"]) };
|
|
581
|
+
/**
|
|
582
|
+
* `pnpm exec <command>`, https://pnpm.io/cli/exec, which hands on every later word, read where
|
|
583
|
+
* no option of pnpm's stands before it, which may run it in another directory or a shell.
|
|
584
|
+
*/
|
|
585
|
+
const pnpmExec = (words) => {
|
|
586
|
+
const read = optionsOf(words, PNPM);
|
|
587
|
+
if (read === undefined || isUnknown(read)) {
|
|
588
|
+
return read;
|
|
589
|
+
}
|
|
590
|
+
return read.at > 0 ? undefined : runnerCommand(words, false);
|
|
591
|
+
};
|
|
246
592
|
/**
|
|
247
|
-
* `pnpm
|
|
248
|
-
*
|
|
593
|
+
* `pnpm dlx [--package <name>]... <command>`, https://pnpm.io/cli/dlx, and `pnpx`, its alias:
|
|
594
|
+
* https://pnpm.io/cli/dlx#pnpx. Another option of pnpm's stands only unread.
|
|
249
595
|
*/
|
|
596
|
+
const pnpmDlx = (words) => {
|
|
597
|
+
const read = optionsOf(words, PNPM_DLX_OPTIONS);
|
|
598
|
+
if (read === undefined || isUnknown(read)) {
|
|
599
|
+
return read;
|
|
600
|
+
}
|
|
601
|
+
return [...read.named].some((name) => name !== "--package")
|
|
602
|
+
? undefined
|
|
603
|
+
: after(read.at, runnerCommand(words.slice(read.at), !read.values.has("--package")));
|
|
604
|
+
};
|
|
250
605
|
const pnpm = subcommands(new Map([
|
|
251
|
-
["exec",
|
|
252
|
-
[
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
},
|
|
260
|
-
],
|
|
261
|
-
]));
|
|
606
|
+
["exec", pnpmExec],
|
|
607
|
+
["dlx", pnpmDlx],
|
|
608
|
+
]), PNPM);
|
|
609
|
+
/** yarn's own options before its subcommand: https://yarnpkg.com/cli */
|
|
610
|
+
const YARN = {
|
|
611
|
+
valued: new Set(["--cwd"]),
|
|
612
|
+
flags: new Set(["-v", "-h", ...long("version", "help")]),
|
|
613
|
+
};
|
|
262
614
|
/**
|
|
263
615
|
* `yarn node`, https://yarnpkg.com/cli/node, and `yarn dlx [-p <name>]... <command>`,
|
|
264
616
|
* https://yarnpkg.com/cli/dlx, whose command runs in the current directory.
|
|
265
617
|
*/
|
|
266
618
|
const yarn = subcommands(new Map([
|
|
267
|
-
[
|
|
619
|
+
[
|
|
620
|
+
"node",
|
|
621
|
+
(words) => ({ kind: "command", words: [{ value: "node", text: "node" }, ...words], at: -1 }),
|
|
622
|
+
],
|
|
268
623
|
[
|
|
269
624
|
"dlx",
|
|
270
625
|
(words) => {
|
|
271
|
-
const read = optionsOf(words,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return read === undefined
|
|
276
|
-
? undefined
|
|
277
|
-
: runnerCommand(words.slice(read.at), !read.values.has("-p") && !read.values.has("--package"));
|
|
626
|
+
const read = optionsOf(words, YARN_DLX);
|
|
627
|
+
return read === undefined || isUnknown(read)
|
|
628
|
+
? read
|
|
629
|
+
: after(read.at, runnerCommand(words.slice(read.at), !read.values.has("-p") && !read.values.has("--package")));
|
|
278
630
|
},
|
|
279
631
|
],
|
|
280
|
-
]));
|
|
632
|
+
]), YARN);
|
|
281
633
|
/** `bunx [-p <name>] <command>[@version] [arguments]`, https://bun.com/docs/pm/bunx */
|
|
282
634
|
const bunx = (words) => {
|
|
283
|
-
const read = optionsOf(words,
|
|
284
|
-
return read === undefined
|
|
635
|
+
const read = optionsOf(words, BUNX);
|
|
636
|
+
return read === undefined || isUnknown(read)
|
|
637
|
+
? read
|
|
638
|
+
: after(read.at, runnerCommand(words.slice(read.at), true));
|
|
639
|
+
};
|
|
640
|
+
/** `bun x`, which is `bunx`, and else bun's own command line. */
|
|
641
|
+
const bun = (words) => words[0]?.value === "x" ? after(1, bunx(words.slice(1))) : runtime(BUN)(words);
|
|
642
|
+
const NO_LINES = { at: new Map(), input: false };
|
|
643
|
+
/** The places `at`, each a whole word that is a command line. */
|
|
644
|
+
function wholeWords(at) {
|
|
645
|
+
return new Map(at.map((one) => [one, ""]));
|
|
646
|
+
}
|
|
647
|
+
/** Whether a word's written text, the text beside its parts, writes a path: text other than `/`, `\`, `.` and `..`. */
|
|
648
|
+
function writesPath(written) {
|
|
649
|
+
return (written ?? "")
|
|
650
|
+
.split(/[/\\]/u)
|
|
651
|
+
.some((segment) => !["", ".", ".."].includes(segment.replaceAll(PART, "")));
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* A shell's options: POSIX sh's one-letter ones and the `set` ones bash takes, which may share a
|
|
655
|
+
* word and may start with `+`, https://pubs.opengroup.org/onlinepubs/9799919799/utilities/sh.html,
|
|
656
|
+
* and bash's `-o`, `-O`, `--rcfile` and `--init-file`, which take the next word, and its long
|
|
657
|
+
* flags: https://www.gnu.org/software/bash/manual/html_node/Invoking-Bash.html
|
|
658
|
+
*/
|
|
659
|
+
const SHELL_OPTIONS = {
|
|
660
|
+
valued: new Set(["-o", "-O", "--rcfile", "--init-file"]),
|
|
661
|
+
flags: new Set([
|
|
662
|
+
...letters("abCefhimnuvxlsrBEHPTckptD"),
|
|
663
|
+
...long("login", "norc", "noprofile", "posix", "restricted", "verbose", "noediting"),
|
|
664
|
+
...long("debugger", "dump-po-strings", "dump-strings", "help", "version"),
|
|
665
|
+
]),
|
|
666
|
+
attached: true,
|
|
667
|
+
};
|
|
668
|
+
/** A shell's one-letter options that give its operand as a command line, and the one that has it read its standard input. */
|
|
669
|
+
const SHELL_LINES = new Set(["-c"]);
|
|
670
|
+
const SHELL_INPUT = "-s";
|
|
671
|
+
/**
|
|
672
|
+
* A shell's command lines: with an option holding `c`, or a dynamic word before its first
|
|
673
|
+
* operand whose written text is only letters, `-` and `+`, each such word and that operand; its
|
|
674
|
+
* standard input where neither stands and no operand names a script, or an option holding `s`
|
|
675
|
+
* or such a word may say so; else its first operand is its script, appended where none stands,
|
|
676
|
+
* its options read by {@link SHELL_OPTIONS}. This is the shell's own scan, not
|
|
677
|
+
* {@link scanOptions}: `-c` is a flag whose operand is the command line, never an attached value.
|
|
678
|
+
*/
|
|
679
|
+
function shellLines(words) {
|
|
680
|
+
const dynamic = [];
|
|
681
|
+
let command = false;
|
|
682
|
+
let input = false;
|
|
683
|
+
let unread = false;
|
|
684
|
+
let operand = words.length;
|
|
685
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
686
|
+
const value = words[at]?.value;
|
|
687
|
+
const written = words[at]?.written?.replaceAll(PART, "");
|
|
688
|
+
if (value === undefined && (written === undefined || /^[A-Za-z+-]*$/u.test(written))) {
|
|
689
|
+
dynamic.push(at);
|
|
690
|
+
}
|
|
691
|
+
else if (value === undefined) {
|
|
692
|
+
operand = at;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
else if (value === "--" || value === "-" || !/^[-+]/u.test(value)) {
|
|
696
|
+
operand = value === "--" || value === "-" ? at + 1 : at;
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
const named = value.startsWith("--")
|
|
701
|
+
? [value]
|
|
702
|
+
: Array.from(value.slice(1), (one) => `-${one}`);
|
|
703
|
+
command ||= value.startsWith("-") && named.some((one) => SHELL_LINES.has(one));
|
|
704
|
+
input ||= value.startsWith("-") && named.includes(SHELL_INPUT);
|
|
705
|
+
unread ||= named.some((one) => !SHELL_OPTIONS.flags.has(one) && !SHELL_OPTIONS.valued.has(one));
|
|
706
|
+
at += SHELL_OPTIONS.valued.has(named.at(-1) ?? "") ? 1 : 0;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const lines = command || dynamic.length > 0;
|
|
710
|
+
const script = command || input ? undefined : words[operand];
|
|
711
|
+
return {
|
|
712
|
+
at: wholeWords(lines ? [...dynamic, ...(operand < words.length ? [operand] : [])] : []),
|
|
713
|
+
input: !command && (input || dynamic.length > 0 || operand >= words.length),
|
|
714
|
+
appended: !input && operand >= words.length,
|
|
715
|
+
unwritten: unread || (script !== undefined && script.value === undefined && !writesPath(script.written)),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/** Every word after the program word, each a command line. */
|
|
719
|
+
function everyWord(words) {
|
|
720
|
+
return { at: wholeWords(words.map((_, at) => at)), input: false };
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The values of the options `lines` among a tool's options `options`, each a command line, as
|
|
724
|
+
* {@link scanOptions} reads them before its first operand or, where `later`, before a `--`,
|
|
725
|
+
* each with the text its word writes before it. Where an option `options` does not hold
|
|
726
|
+
* stands, every later word may be one.
|
|
727
|
+
*/
|
|
728
|
+
function optionLines(options, lines, later = false) {
|
|
729
|
+
return (words) => {
|
|
730
|
+
const at = new Map();
|
|
731
|
+
for (let from = 0; from < words.length;) {
|
|
732
|
+
const scan = scanOptions(words, options, from);
|
|
733
|
+
for (const { name, value } of scan.options) {
|
|
734
|
+
if (value !== undefined && value.at < words.length && lines.has(name)) {
|
|
735
|
+
at.set(value.at, value.prefix);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (scan.stop === "unread") {
|
|
739
|
+
for (let one = scan.at; one < words.length; one += 1) {
|
|
740
|
+
at.set(one, "");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (scan.stop !== "operand" || !later) {
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
from = scan.at + 1;
|
|
747
|
+
}
|
|
748
|
+
return { at, input: false };
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* The lines of a tool whose subcommand `names`, after its own options `options`, reads its
|
|
753
|
+
* words, and those options, as `read` does; where one of those options is unread, any word may be one.
|
|
754
|
+
*/
|
|
755
|
+
function subcommandLines(names, read, options) {
|
|
756
|
+
return (words) => {
|
|
757
|
+
const sub = options === undefined ? 0 : operandAt(words, options);
|
|
758
|
+
if (sub === undefined) {
|
|
759
|
+
return everyWord(words);
|
|
760
|
+
}
|
|
761
|
+
const name = words[sub]?.value;
|
|
762
|
+
if (name === undefined || !names.has(name)) {
|
|
763
|
+
return NO_LINES;
|
|
764
|
+
}
|
|
765
|
+
const found = read(words.slice(sub + 1), words.slice(0, sub)).at;
|
|
766
|
+
return { at: new Map([...found].map(([at, prefix]) => [at + sub + 1, prefix])), input: false };
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
/** The lines of `pnpm dlx` or `pnpx`, its words, after pnpm's options `before`, as {@link pnpmLines} reads them. */
|
|
770
|
+
function dlxLines(words, before = []) {
|
|
771
|
+
const scans = [scanOptions(before, PNPM), scanOptions(words, PNPM_DLX_OPTIONS)];
|
|
772
|
+
return scans.some((scan) => scan.stop === "unread" || scan.options.some(({ name }) => PNPM_LINES.has(name)))
|
|
773
|
+
? everyWord(words)
|
|
774
|
+
: NO_LINES;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* The lines of `pnpm exec` and `pnpm dlx`: every operand where one of {@link PNPM_LINES}
|
|
778
|
+
* stands among pnpm's options before or after the subcommand, or where one of them is unread.
|
|
779
|
+
*/
|
|
780
|
+
const pnpmLines = subcommandLines(new Set(["exec", "dlx"]), dlxLines, PNPM);
|
|
781
|
+
/** Each of `texts` as a pattern of each place it stands in a word, or `undefined` where none is. */
|
|
782
|
+
function textsPattern(texts) {
|
|
783
|
+
const written = texts.filter((text) => text !== "");
|
|
784
|
+
return written.length === 0
|
|
785
|
+
? undefined
|
|
786
|
+
: new RegExp(written.map((text) => text.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("|"), "gu");
|
|
787
|
+
}
|
|
788
|
+
/** The words of an appending tool with an option or a construct no row reads. */
|
|
789
|
+
const UNREAD = { starts: undefined, replaced: undefined, from: 0 };
|
|
790
|
+
/** The long options named `names`. */
|
|
791
|
+
function long(...names) {
|
|
792
|
+
return names.map((name) => `--${name}`);
|
|
793
|
+
}
|
|
794
|
+
/** The one-letter options `letters` name. */
|
|
795
|
+
function letters(written) {
|
|
796
|
+
return Array.from(written, (letter) => `-${letter}`);
|
|
797
|
+
}
|
|
798
|
+
/** https://www.gnu.org/software/findutils/manual/html_node/find_html/xargs-options.html */
|
|
799
|
+
const XARGS = {
|
|
800
|
+
valued: new Set([
|
|
801
|
+
...letters("adEILnPs"),
|
|
802
|
+
...long("arg-file", "delimiter", "max-args", "max-procs", "max-chars", "process-slot-var"),
|
|
803
|
+
]),
|
|
804
|
+
flags: new Set([
|
|
805
|
+
...letters("0prtxo"),
|
|
806
|
+
...long("null", "interactive", "no-run-if-empty", "verbose", "exit", "open-tty", "show-limits"),
|
|
807
|
+
]),
|
|
808
|
+
attached: true,
|
|
809
|
+
optional: new Set(["-e", "-i", "-l", ...long("eof", "replace", "max-lines")]),
|
|
810
|
+
};
|
|
811
|
+
/**
|
|
812
|
+
* `xargs [option]... [command [argument]...]`: its command starts after its options, and `-I`,
|
|
813
|
+
* `-i` and `--replace` name the text it replaces with each path it reads, `{}` where `-i` or
|
|
814
|
+
* `--replace` names none. With no command it runs `echo`.
|
|
815
|
+
*/
|
|
816
|
+
function xargs(words) {
|
|
817
|
+
const read = optionsOf(words, XARGS);
|
|
818
|
+
if (read === undefined || isUnknown(read)) {
|
|
819
|
+
return UNREAD;
|
|
820
|
+
}
|
|
821
|
+
const defaulted = [...(read.values.get("-i") ?? []), ...(read.values.get("--replace") ?? [])];
|
|
822
|
+
return {
|
|
823
|
+
starts: [read.at],
|
|
824
|
+
replaced: textsPattern([
|
|
825
|
+
...(read.values.get("-I") ?? []),
|
|
826
|
+
...defaulted.map((text) => (text === "" ? "{}" : text)),
|
|
827
|
+
]),
|
|
828
|
+
from: read.at,
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
/** `find`'s actions that run a command up to a `;` or `+`, each `{}` in its words a path it finds: https://www.gnu.org/software/findutils/manual/html_node/find_html/Single-File.html */
|
|
832
|
+
const FIND_ACTIONS = new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
833
|
+
/**
|
|
834
|
+
* GNU find's options and primaries that take the next word, two for `-fprintf`:
|
|
835
|
+
* https://www.gnu.org/software/findutils/manual/html_node/find_html/Primary-Index.html
|
|
836
|
+
*/
|
|
837
|
+
const FIND_VALUED = new Set([
|
|
838
|
+
...["-D", "-name", "-iname", "-path", "-ipath", "-wholename", "-iwholename", "-regex"],
|
|
839
|
+
...["-iregex", "-lname", "-ilname", "-type", "-xtype", "-user", "-group", "-uid", "-gid"],
|
|
840
|
+
...["-perm", "-size", "-mtime", "-atime", "-ctime", "-mmin", "-amin", "-cmin", "-newer"],
|
|
841
|
+
...["-anewer", "-cnewer", "-inum", "-links", "-samefile", "-used", "-fstype", "-context"],
|
|
842
|
+
...["-maxdepth", "-mindepth", "-printf", "-fprint", "-fprint0", "-fls", "-regextype"],
|
|
843
|
+
...["-files0-from", "-fprintf"],
|
|
844
|
+
]);
|
|
845
|
+
/** find's `-newerXY` primaries, which take the next word too. */
|
|
846
|
+
const FIND_NEWER = /^-newer[aBcm][aBcmt]$/u;
|
|
847
|
+
/**
|
|
848
|
+
* `find`'s words: its paths, up to the first that starts with `-` or is `(` or `!`, then its
|
|
849
|
+
* expression. A dynamic word that is no primary's value, nor a path the expression follows,
|
|
850
|
+
* may be an action, a construct no row reads.
|
|
851
|
+
*/
|
|
852
|
+
function find(words) {
|
|
853
|
+
const starts = [];
|
|
854
|
+
let paths = true;
|
|
855
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
856
|
+
const value = words[at]?.value;
|
|
857
|
+
paths &&= value === undefined || !(value.startsWith("-") || value === "(" || value === "!");
|
|
858
|
+
const next = words[at + 1]?.value;
|
|
859
|
+
if (value === undefined) {
|
|
860
|
+
if (paths && (next === undefined ? at + 1 === words.length : /^[-(!]/u.test(next))) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
return UNREAD;
|
|
864
|
+
}
|
|
865
|
+
if (FIND_VALUED.has(value) || FIND_NEWER.test(value)) {
|
|
866
|
+
at += value === "-fprintf" ? 2 : 1;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (FIND_ACTIONS.has(value)) {
|
|
870
|
+
starts.push(at + 1);
|
|
871
|
+
at = words.findIndex((word, end) => end > at &&
|
|
872
|
+
(word.value === ";" || (word.value === "+" && words[end - 1]?.value === "{}")));
|
|
873
|
+
at = at < 0 ? words.length : at;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return { starts, replaced: /\{\}/gu, from: starts[0] ?? words.length };
|
|
877
|
+
}
|
|
878
|
+
/** GNU parallel's options naming a text other than a `{…}` that it replaces with a part of each path. */
|
|
879
|
+
const PARALLEL_REPLACING = [
|
|
880
|
+
"-I",
|
|
881
|
+
...long("er", "extensionreplace", "bnr", "basenamereplace", "dnr", "dirnamereplace"),
|
|
882
|
+
...long("bner", "basenameextensionreplace", "seqreplace", "slotreplace"),
|
|
883
|
+
];
|
|
884
|
+
/** https://www.gnu.org/software/parallel/parallel.html */
|
|
885
|
+
const PARALLEL = {
|
|
886
|
+
valued: new Set([
|
|
887
|
+
...PARALLEL_REPLACING,
|
|
888
|
+
...letters("jNnLaCdSE"),
|
|
889
|
+
...long("jobs", "max-args", "max-lines", "arg-file", "colsep", "delimiter", "sshlogin"),
|
|
890
|
+
...long("joblog", "results", "delay", "timeout", "halt", "tmpdir", "workdir", "eof"),
|
|
891
|
+
]),
|
|
892
|
+
flags: new Set([
|
|
893
|
+
...letters("kvq0uXmr"),
|
|
894
|
+
...long("keep-order", "verbose", "bar", "progress", "eta", "tag", "dry-run", "null"),
|
|
895
|
+
...long("quote", "ungroup", "group", "line-buffer", "lb", "xargs", "no-notice"),
|
|
896
|
+
...long("will-cite", "shuf", "plus", "no-run-if-empty", "pipe"),
|
|
897
|
+
]),
|
|
898
|
+
attached: true,
|
|
899
|
+
};
|
|
900
|
+
/**
|
|
901
|
+
* `parallel [option]... [command [argument]...] [::: argument... | :::: file...]`: its command
|
|
902
|
+
* starts after its options and ends at the first `:::` or `::::`, and every `{…}` in it, each
|
|
903
|
+
* one of the replacement strings it documents, and each text its replacing options name
|
|
904
|
+
* stand for a part of each path it is handed. A `{=…=}`, the path a perl expression computes,
|
|
905
|
+
* is a construct no row reads.
|
|
906
|
+
*/
|
|
907
|
+
function parallel(words) {
|
|
908
|
+
const read = optionsOf(words, PARALLEL);
|
|
909
|
+
if (read === undefined || isUnknown(read)) {
|
|
910
|
+
return UNREAD;
|
|
911
|
+
}
|
|
912
|
+
const end = words.findIndex((word, at) => at >= read.at && /^::::?\+?$/u.test(word.value ?? ""));
|
|
913
|
+
const command = words.slice(read.at, end < 0 ? undefined : end);
|
|
914
|
+
// The word parallel is handed, its quotes removed, as `'{''=…=}'` writes `{=…=}`.
|
|
915
|
+
if (command.some((word) => (word.value ?? word.written ?? word.text).includes("{="))) {
|
|
916
|
+
return UNREAD;
|
|
917
|
+
}
|
|
918
|
+
const texts = textsPattern(PARALLEL_REPLACING.flatMap((name) => read.values.get(name) ?? []));
|
|
919
|
+
return {
|
|
920
|
+
starts: [read.at],
|
|
921
|
+
replaced: new RegExp([/\{[^{}\s]*\}/u.source, ...(texts === undefined ? [] : [texts.source])].join("|"), "gu"),
|
|
922
|
+
from: read.at,
|
|
923
|
+
...(end < 0 ? {} : { end }),
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* `parallel` hands a shell its command's words joined by blanks as one command line; with no
|
|
928
|
+
* command before its `:::` each word after it, and with none at all each line it reads.
|
|
929
|
+
*/
|
|
930
|
+
function parallelLines(words) {
|
|
931
|
+
const { starts, end } = parallel(words);
|
|
932
|
+
const [start] = starts ?? [];
|
|
933
|
+
if (start === undefined) {
|
|
934
|
+
return everyWord(words);
|
|
935
|
+
}
|
|
936
|
+
if (start === end) {
|
|
937
|
+
return { at: wholeWords(words.slice(start + 1).map((_, at) => start + 1 + at)), input: false };
|
|
938
|
+
}
|
|
939
|
+
return start === words.length
|
|
940
|
+
? { at: new Map(), input: true }
|
|
941
|
+
: { at: new Map(), input: false, joined: [[start, end ?? words.length]] };
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* concurrently's options, read by yargs before and after its commands, each a long one's
|
|
945
|
+
* `=` spelling too: https://github.com/open-cli-tools/concurrently/blob/main/bin/index.ts
|
|
946
|
+
*/
|
|
947
|
+
const CONCURRENTLY = {
|
|
948
|
+
valued: new Set([
|
|
949
|
+
...["-m", "-n", "-s", "-p", "-c", "-l", "-t"],
|
|
950
|
+
...long("max-processes", "names", "success", "hide", "shell", "teardown", "kill-signal"),
|
|
951
|
+
...long("kill-timeout", "prefix", "prefix-colors", "prefix-length", "timestamp-format"),
|
|
952
|
+
...long("restart-tries", "restart-after", "default-input-target", "name-separator"),
|
|
953
|
+
]),
|
|
954
|
+
flags: new Set([
|
|
955
|
+
...["-r", "-g", "-P", "-k", "-i", "-h", "-v", "-V"],
|
|
956
|
+
...long("raw", "no-color", "group", "timings", "passthrough-arguments", "kill-others"),
|
|
957
|
+
...long("kill-others-on-fail", "pad-prefix", "handle-input", "help", "version"),
|
|
958
|
+
]),
|
|
285
959
|
};
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
|
|
960
|
+
/** concurrently's `--teardown`, whose value is a command line it runs before it exits. */
|
|
961
|
+
const TEARDOWN = "--teardown";
|
|
962
|
+
/**
|
|
963
|
+
* The words with a `{N}`, `{@}` or `{*}`, which concurrently replaces with the arguments after
|
|
964
|
+
* its `--` under `-P`, a part only the run knows: a `CONCURRENTLY_PASSTHROUGH_ARGUMENTS`
|
|
965
|
+
* variable, which yargs's `.env` reads, sets `-P` where no word writes it.
|
|
966
|
+
*/
|
|
967
|
+
const PLACEHOLDER = /\{(?:\d+|@|\*)\}/gu;
|
|
968
|
+
/**
|
|
969
|
+
* concurrently's command lines: each word that is no option nor an option's value, each
|
|
970
|
+
* `--teardown` value, and the words after a `--` but under `-P`, where they are its commands'
|
|
971
|
+
* arguments. From an option {@link CONCURRENTLY} does not list, every word may be one.
|
|
972
|
+
*/
|
|
973
|
+
function concurrentlyLines(words) {
|
|
974
|
+
const at = new Map();
|
|
975
|
+
let passthrough = false;
|
|
976
|
+
for (let one = 0; one < words.length; one += 1) {
|
|
977
|
+
const value = words[one]?.value;
|
|
978
|
+
if (value === "--") {
|
|
979
|
+
for (let later = one + 1; !passthrough && later < words.length; later += 1) {
|
|
980
|
+
at.set(later, "");
|
|
981
|
+
}
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
if (value === undefined || value === "-" || !value.startsWith("-")) {
|
|
985
|
+
at.set(one, "");
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
const equals = value.startsWith("--") ? value.indexOf("=") : -1;
|
|
989
|
+
const name = equals < 0 ? value : value.slice(0, equals);
|
|
990
|
+
if (equals < 0 && CONCURRENTLY.flags.has(name)) {
|
|
991
|
+
passthrough ||= name === "-P" || name === "--passthrough-arguments";
|
|
992
|
+
}
|
|
993
|
+
else if (CONCURRENTLY.valued.has(name)) {
|
|
994
|
+
const valueAt = equals < 0 ? (one += 1) : one;
|
|
995
|
+
if (name === TEARDOWN && valueAt < words.length) {
|
|
996
|
+
at.set(valueAt, equals < 0 ? "" : `${TEARDOWN}=`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
for (let later = one; later < words.length; later += 1) {
|
|
1001
|
+
at.set(later, "");
|
|
1002
|
+
}
|
|
1003
|
+
return { at, input: false, unwritten: true, replaced: PLACEHOLDER };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return { at, input: false, replaced: PLACEHOLDER };
|
|
1007
|
+
}
|
|
1008
|
+
/** `eval`'s operands joined by blanks, one command line: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#eval */
|
|
1009
|
+
function evalLines(words) {
|
|
1010
|
+
const from = words[0]?.value === "--" ? 1 : 0;
|
|
1011
|
+
return from >= words.length ? NO_LINES : { ...NO_LINES, joined: [[from, words.length]] };
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* The script `.` and `source` run in the shell itself, their first operand: one written with
|
|
1015
|
+
* no path runs a file no word writes, as `bash "$F"` does, and so does an option, which POSIX
|
|
1016
|
+
* gives them none of: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#dot
|
|
1017
|
+
*/
|
|
1018
|
+
function sourceLines(words) {
|
|
1019
|
+
const script = words[words[0]?.value === "--" ? 1 : 0];
|
|
1020
|
+
return {
|
|
1021
|
+
...NO_LINES,
|
|
1022
|
+
unwritten: script !== undefined &&
|
|
1023
|
+
(script.value === undefined
|
|
1024
|
+
? !writesPath(script.written)
|
|
1025
|
+
: script.value.startsWith("-") && script.value !== "--"),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* The row of a launcher whose reader is `read`, which runs a command after `after` where
|
|
1030
|
+
* given, read after its own options `options`.
|
|
1031
|
+
*/
|
|
1032
|
+
function launcher(read, after = [], lines, options) {
|
|
1033
|
+
return {
|
|
1034
|
+
...(read === undefined ? {} : { read }),
|
|
1035
|
+
launches: {
|
|
1036
|
+
...(after.length === 0 ? {} : { after: new Set(after) }),
|
|
1037
|
+
...(lines === undefined ? {} : { lines }),
|
|
1038
|
+
...(options === undefined ? {} : { options }),
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
/** The row of a runtime. */
|
|
1043
|
+
function runs(line) {
|
|
1044
|
+
return { read: runtime(line), runtime: line };
|
|
289
1045
|
}
|
|
290
1046
|
/** The row of a tool that names its configuration file by `options` anywhere before a `--`. */
|
|
291
1047
|
function configuredBy(...options) {
|
|
292
1048
|
return { configurations: { valued: new Set(options) } };
|
|
293
1049
|
}
|
|
294
|
-
/** The row of
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
1050
|
+
/** The row of a shell, whose `-c` runs its operand as a command line. */
|
|
1051
|
+
const SHELL = { launches: { line: true, lines: shellLines } };
|
|
1052
|
+
/**
|
|
1053
|
+
* The reader of a tool that runs the command after its options `options`, the first operand,
|
|
1054
|
+
* or where `ended` the first word after a `--`, before which only its options stand.
|
|
1055
|
+
*/
|
|
1056
|
+
function afterOptions(options, ended = false) {
|
|
1057
|
+
return (words) => {
|
|
1058
|
+
const end = words.findIndex((word) => word.value === "--");
|
|
1059
|
+
const read = optionsOf(ended && end >= 0 ? words.slice(0, end) : words, options);
|
|
1060
|
+
if (read === undefined || isUnknown(read)) {
|
|
1061
|
+
return read;
|
|
1062
|
+
}
|
|
1063
|
+
if (ended && end >= 0 && read.at !== end) {
|
|
1064
|
+
return undefined;
|
|
1065
|
+
}
|
|
1066
|
+
const at = ended && end >= 0 ? end + 1 : read.at;
|
|
1067
|
+
const program = words[at];
|
|
1068
|
+
return program?.value === undefined || program.value.startsWith("-")
|
|
1069
|
+
? undefined
|
|
1070
|
+
: { kind: "command", words: words.slice(at), at };
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* c8's options, `c8 [opts] [script] [opts]`, https://github.com/bcoe/c8#cli-options, read by
|
|
1075
|
+
* yargs, which clusters no valued option's value: https://github.com/bcoe/c8/blob/main/lib/parse-args.js
|
|
1076
|
+
*/
|
|
1077
|
+
const C8 = {
|
|
1078
|
+
valued: new Set([
|
|
1079
|
+
...["-c", "-r", "-o", "-n", "-x", "-e"],
|
|
1080
|
+
...long("config", "reporter", "reports-dir", "report-dir", "src", "include", "exclude"),
|
|
1081
|
+
...long("extension", "branches", "functions", "lines", "statements", "temp-directory"),
|
|
1082
|
+
...long("resolve", "wrapper-length"),
|
|
1083
|
+
]),
|
|
1084
|
+
flags: new Set([
|
|
1085
|
+
"-a",
|
|
1086
|
+
...long("all", "exclude-node-modules", "exclude-after-remap", "skip-full", "check-coverage"),
|
|
1087
|
+
...long("per-file", "100", "clean", "omit-relative", "allowExternal", "merge-async"),
|
|
1088
|
+
...long("experimental-monocart", "no-clean", "no-exclude-node-modules", "no-omit-relative"),
|
|
1089
|
+
]),
|
|
1090
|
+
};
|
|
1091
|
+
/**
|
|
1092
|
+
* nyc's options, `nyc [options] [bin-to-instrument]`, https://github.com/istanbuljs/nyc#usage,
|
|
1093
|
+
* each of `@istanbuljs/schema`'s with its alias: https://github.com/istanbuljs/schema/blob/master/index.js
|
|
1094
|
+
*/
|
|
1095
|
+
const NYC = {
|
|
1096
|
+
valued: new Set([
|
|
1097
|
+
...["-t", "-x", "-n", "-e", "-i", "-r"],
|
|
1098
|
+
...long("cwd", "temp-dir", "exclude", "include", "extension", "ignore-class-methods"),
|
|
1099
|
+
...long("parser-plugins", "require", "reporter", "report-dir", "cache-dir", "nycrc-path"),
|
|
1100
|
+
...long("branches", "functions", "lines", "statements"),
|
|
1101
|
+
]),
|
|
1102
|
+
flags: new Set([
|
|
1103
|
+
...["-s", "-a", "-c"],
|
|
1104
|
+
...long("exclude-node-modules", "auto-wrap", "es-modules", "compact", "preserve-comments"),
|
|
1105
|
+
...long("produce-source-map", "source-map", "instrument", "exclude-after-remap", "per-file"),
|
|
1106
|
+
...long("check-coverage", "show-process-tree", "skip-empty", "skip-full", "silent", "all"),
|
|
1107
|
+
...long("eager", "cache", "babel-cache", "use-spawn-wrap", "hook-require", "clean"),
|
|
1108
|
+
...long("hook-run-in-context", "hook-run-in-this-context", "in-place", "exit-on-error"),
|
|
1109
|
+
...long("delete", "complete-copy", "no-clean", "no-cache", "no-instrument"),
|
|
1110
|
+
]),
|
|
1111
|
+
};
|
|
1112
|
+
/**
|
|
1113
|
+
* dotenv-cli's options, `dotenv [-e <path>] [-v <name>=<value>] [-c [env]] [-- command]`,
|
|
1114
|
+
* https://github.com/entropitor/dotenv-cli#usage. minimist reads a flag's next word as its
|
|
1115
|
+
* value, so only `-e`, `-v` and `-c` are read where no `--` stands; `-p` and `--debug` run nothing.
|
|
1116
|
+
*/
|
|
1117
|
+
const DOTENV = {
|
|
1118
|
+
valued: new Set(["-e", "-v", "-c"]),
|
|
1119
|
+
flags: new Set(["-o", "--override", "--no-expand", "-q"]),
|
|
1120
|
+
optional: new Set(["--quiet"]),
|
|
1121
|
+
};
|
|
1122
|
+
/** dotenv-cli, reading its flags only before a `--`. */
|
|
1123
|
+
const dotenv = (words) => words.some((word) => word.value === "--")
|
|
1124
|
+
? afterOptions(DOTENV, true)(words)
|
|
1125
|
+
: afterOptions({ valued: DOTENV.valued, flags: new Set() })(words);
|
|
1126
|
+
/**
|
|
1127
|
+
* env-cmd's options, `env-cmd [options] -- <command> [...args]`, read by commander, which
|
|
1128
|
+
* clusters short options: https://github.com/toddbluhm/env-cmd#-help. `-e` takes every word
|
|
1129
|
+
* after it that starts with no `-`, so it is read only before a `--`.
|
|
1130
|
+
*/
|
|
1131
|
+
const ENV_CMD = {
|
|
1132
|
+
valued: new Set(["-f", "--file"]),
|
|
1133
|
+
flags: new Set([
|
|
1134
|
+
"-x",
|
|
1135
|
+
...long("expand-envs", "recursive", "fallback", "no-override", "silent", "use-shell"),
|
|
1136
|
+
"--verbose",
|
|
1137
|
+
]),
|
|
1138
|
+
attached: true,
|
|
1139
|
+
};
|
|
1140
|
+
/** env-cmd, whose `-e` and `--environments` stand only before a `--`. */
|
|
1141
|
+
const envCmd = (words) => words.some((word) => word.value === "--")
|
|
1142
|
+
? afterOptions({ ...ENV_CMD, valued: new Set([...ENV_CMD.valued, "-e", "--environments"]) }, true)(words)
|
|
1143
|
+
: afterOptions(ENV_CMD)(words);
|
|
1144
|
+
/** env-cmd's `--use-shell` hands its command's words, joined by blanks, to a shell. */
|
|
1145
|
+
function envCmdLines(words) {
|
|
1146
|
+
const reading = envCmd(words);
|
|
1147
|
+
return reading?.kind === "command" && words.some((word) => word.value === "--use-shell")
|
|
1148
|
+
? { at: new Map(), input: false, joined: [[reading.at, words.length]] }
|
|
1149
|
+
: NO_LINES;
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* `dotenvx run [options] -- <command>`, https://dotenvx.com/docs/advanced/run, whose command
|
|
1153
|
+
* stands after `--`: https://github.com/dotenvx/dotenvx/blob/main/src/cli/dotenvx.js
|
|
1154
|
+
*/
|
|
1155
|
+
const DOTENVX_RUN = {
|
|
1156
|
+
valued: new Set([
|
|
1157
|
+
...["-e", "-f", "-fk"],
|
|
1158
|
+
...long("env", "env-file", "env-keys-file", "convention", "ignore", "token"),
|
|
1159
|
+
]),
|
|
1160
|
+
flags: new Set(["-o", ...long("overload", "strict", "no-armor", "no-native")]),
|
|
1161
|
+
attached: true,
|
|
1162
|
+
};
|
|
1163
|
+
/** dotenvx's own options before its subcommand: https://dotenvx.com/docs/cli */
|
|
1164
|
+
const DOTENVX = {
|
|
1165
|
+
valued: new Set(["-l", "--log-level"]),
|
|
1166
|
+
flags: new Set([
|
|
1167
|
+
"-q",
|
|
1168
|
+
"-v",
|
|
1169
|
+
"-d",
|
|
1170
|
+
"-V",
|
|
1171
|
+
"-h",
|
|
1172
|
+
...long("quiet", "verbose", "debug", "version", "help"),
|
|
1173
|
+
]),
|
|
1174
|
+
};
|
|
1175
|
+
/**
|
|
1176
|
+
* cross-env-shell, which runs its words after the assignments, joined by blanks, as one
|
|
1177
|
+
* command line: https://github.com/kentcdodds/cross-env#cross-env-vs-cross-env-shell
|
|
1178
|
+
*/
|
|
1179
|
+
function crossEnvShellLines(words) {
|
|
1180
|
+
const at = words.findIndex((word) => !/^\w+=/u.test(startOf(word)));
|
|
1181
|
+
return at < 0 ? NO_LINES : { at: new Map(), input: false, joined: [[at, words.length]] };
|
|
1182
|
+
}
|
|
1183
|
+
/** The row of each tool, by the name its program word's last segment writes, but its grammar. */
|
|
1184
|
+
const ROWS = new Map([
|
|
1185
|
+
["node", runs(NODE)],
|
|
1186
|
+
["tsx", runs(TSX)],
|
|
1187
|
+
["ts-node", runs(TS_NODE)],
|
|
1188
|
+
["bun", { read: bun, runtime: BUN, launches: { after: new Set(["x"]) } }],
|
|
1189
|
+
["deno", runs(DENO)],
|
|
1190
|
+
["env", launcher(env, [], envLines)],
|
|
1191
|
+
["cross-env", launcher(crossEnv)],
|
|
1192
|
+
["cross-env-shell", launcher(undefined, [], crossEnvShellLines)],
|
|
1193
|
+
["c8", launcher(afterOptions(C8))],
|
|
1194
|
+
["nyc", launcher(afterOptions(NYC))],
|
|
1195
|
+
["dotenv", launcher(dotenv)],
|
|
1196
|
+
["env-cmd", launcher(envCmd, [], envCmdLines)],
|
|
1197
|
+
[
|
|
1198
|
+
"dotenvx",
|
|
1199
|
+
launcher(subcommands(new Map([["run", afterOptions(DOTENVX_RUN, true)]]), DOTENVX), ["run"], undefined, DOTENVX),
|
|
1200
|
+
],
|
|
301
1201
|
[
|
|
302
1202
|
"npx",
|
|
303
1203
|
{
|
|
304
|
-
|
|
1204
|
+
...launcher(npx),
|
|
305
1205
|
// `-c` takes a command line, which is read as a configuration file's name as well, so
|
|
306
1206
|
// every configuration `fixtures/commands/observed.json` records stays named.
|
|
307
1207
|
configurations: {
|
|
@@ -310,10 +1210,28 @@ const TOOLS = new Map([
|
|
|
310
1210
|
},
|
|
311
1211
|
},
|
|
312
1212
|
],
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
[
|
|
316
|
-
|
|
1213
|
+
// The readers of npm and pnpm leave their own options before the subcommand unread, as each
|
|
1214
|
+
// of them, `--workspace` and `--filter` among them, may run the command in another directory.
|
|
1215
|
+
[
|
|
1216
|
+
"npm",
|
|
1217
|
+
launcher(npm, ["exec", "x"], subcommandLines(new Set(["exec", "x"]), optionLines(NPM_EXEC, CALL, true), NPM), NPM),
|
|
1218
|
+
],
|
|
1219
|
+
// pnpm and yarn run a package's command of a name that is none of theirs:
|
|
1220
|
+
// https://pnpm.io/pnpm-cli, https://yarnpkg.com/cli/run; `yarn exec` runs its command in the
|
|
1221
|
+
// workspace's root, https://yarnpkg.com/cli/exec
|
|
1222
|
+
["pnpm", launcher(pnpm, [], pnpmLines)],
|
|
1223
|
+
["pnpx", launcher(pnpmDlx, [], (words) => dlxLines(words))],
|
|
1224
|
+
["yarn", launcher(yarn)],
|
|
1225
|
+
["bunx", launcher(bunx)],
|
|
1226
|
+
// The shell's `exec`, `command` and `builtin`, and the wrappers that run their operands.
|
|
1227
|
+
...["exec", "command", "builtin", "nohup", "nice", "time", "timeout", "sudo"].map((name) => [name, launcher(undefined)]),
|
|
1228
|
+
["eval", { launches: { lines: evalLines } }],
|
|
1229
|
+
["concurrently", { launches: { lines: concurrentlyLines } }],
|
|
1230
|
+
...[".", "source"].map((name) => [name, { launches: { line: true, lines: sourceLines } }]),
|
|
1231
|
+
...["sh", "bash", "dash", "zsh", "ksh", "ash"].map((name) => [name, SHELL]),
|
|
1232
|
+
["xargs", { launches: { appends: xargs } }],
|
|
1233
|
+
["find", { launches: { appends: find } }],
|
|
1234
|
+
["parallel", { launches: { appends: parallel, lines: parallelLines } }],
|
|
317
1235
|
// https://eslint.org/docs/latest/use/command-line-interface
|
|
318
1236
|
["eslint", configuredBy("-c", "--config")],
|
|
319
1237
|
// https://prettier.io/docs/cli, whose `-c` is `--check`.
|
|
@@ -331,21 +1249,494 @@ const TOOLS = new Map([
|
|
|
331
1249
|
// https://mochajs.org/#command-line-usage, whose `-c` is `--color`.
|
|
332
1250
|
["mocha", configuredBy("--config")],
|
|
333
1251
|
]);
|
|
334
|
-
/**
|
|
335
|
-
function
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
1252
|
+
/** A runtime's {@link Grammar}, from the same table its reader reads by. */
|
|
1253
|
+
function runtimeGrammar(line, doc) {
|
|
1254
|
+
const { valued, flags, optional } = runtimeOptions(line);
|
|
1255
|
+
const inline = new Set([...(line.inline ?? []), ...(line.runner ?? [])]);
|
|
1256
|
+
const options = {
|
|
1257
|
+
valued: new Set([...valued].filter((name) => !inline.has(name))),
|
|
1258
|
+
flags: new Set([...flags].filter((name) => name !== "-" && !inline.has(name))),
|
|
1259
|
+
...(optional === undefined || optional.size === 0 ? {} : { optional }),
|
|
1260
|
+
};
|
|
1261
|
+
const one = { options, operand: "file", inline, doc };
|
|
1262
|
+
const modes = {
|
|
1263
|
+
...(line.modes === undefined ? {} : { modes: line.modes }),
|
|
1264
|
+
...(line.modeAfter === undefined ? {} : { modeAfter: line.modeAfter }),
|
|
1265
|
+
};
|
|
1266
|
+
return {
|
|
1267
|
+
...one,
|
|
1268
|
+
...modes,
|
|
1269
|
+
subcommands: new Map([...(line.subcommands ?? [])].map((name) => [name, one])),
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Each tool's {@link Grammar}: its options, the object its reader parses with, and how its
|
|
1274
|
+
* other words read, which production reads only through {@link linesOf} and the enumerated
|
|
1275
|
+
* property spells whole. Tools with no table, `sudo` and `nohup` among them, have none: after
|
|
1276
|
+
* them every later word may be a program word.
|
|
1277
|
+
*/
|
|
1278
|
+
const GRAMMARS = new Map([
|
|
1279
|
+
["node", runtimeGrammar(NODE, "https://nodejs.org/api/cli.html")],
|
|
1280
|
+
["tsx", runtimeGrammar(TSX, "https://tsx.is/usage")],
|
|
1281
|
+
["ts-node", runtimeGrammar(TS_NODE, "https://typestrong.org/ts-node/docs/options")],
|
|
1282
|
+
["bun", runtimeGrammar(BUN, "https://bun.com/docs/cli/run")],
|
|
1283
|
+
["deno", runtimeGrammar(DENO, "https://docs.deno.com/runtime/reference/cli/run/")],
|
|
1284
|
+
[
|
|
1285
|
+
"env",
|
|
1286
|
+
{
|
|
1287
|
+
options: ENV,
|
|
1288
|
+
operand: "command",
|
|
1289
|
+
lines: new Set(["-S", "--split-string"]),
|
|
1290
|
+
doc: "https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html",
|
|
1291
|
+
},
|
|
1292
|
+
],
|
|
1293
|
+
[
|
|
1294
|
+
"npx",
|
|
1295
|
+
{
|
|
1296
|
+
options: NPX,
|
|
1297
|
+
operand: "command",
|
|
1298
|
+
lines: CALL,
|
|
1299
|
+
doc: "https://docs.npmjs.com/cli/v11/commands/npx",
|
|
1300
|
+
},
|
|
1301
|
+
],
|
|
1302
|
+
[
|
|
1303
|
+
"npm",
|
|
1304
|
+
{
|
|
1305
|
+
options: NPM,
|
|
1306
|
+
operand: "command",
|
|
1307
|
+
subcommands: new Map(["exec", "x"].map((name) => [
|
|
1308
|
+
name,
|
|
1309
|
+
{
|
|
1310
|
+
options: NPM_EXEC,
|
|
1311
|
+
operand: "command",
|
|
1312
|
+
lines: CALL,
|
|
1313
|
+
later: true,
|
|
1314
|
+
doc: "https://docs.npmjs.com/cli/v11/commands/npm-exec",
|
|
1315
|
+
},
|
|
1316
|
+
])),
|
|
1317
|
+
doc: "https://docs.npmjs.com/cli/v11/using-npm/config",
|
|
1318
|
+
},
|
|
1319
|
+
],
|
|
1320
|
+
[
|
|
1321
|
+
"pnpm",
|
|
1322
|
+
{
|
|
1323
|
+
options: PNPM,
|
|
1324
|
+
operand: "command",
|
|
1325
|
+
subcommands: new Map([
|
|
1326
|
+
[
|
|
1327
|
+
"exec",
|
|
1328
|
+
{ options: PNPM, operand: "command", lines: PNPM_LINES, doc: "https://pnpm.io/cli/exec" },
|
|
1329
|
+
],
|
|
1330
|
+
[
|
|
1331
|
+
"dlx",
|
|
1332
|
+
{
|
|
1333
|
+
options: PNPM_DLX_OPTIONS,
|
|
1334
|
+
operand: "command",
|
|
1335
|
+
lines: PNPM_LINES,
|
|
1336
|
+
doc: "https://pnpm.io/cli/dlx",
|
|
1337
|
+
},
|
|
1338
|
+
],
|
|
1339
|
+
]),
|
|
1340
|
+
lines: PNPM_LINES,
|
|
1341
|
+
doc: "https://pnpm.io/pnpm-cli#options",
|
|
1342
|
+
},
|
|
1343
|
+
],
|
|
1344
|
+
[
|
|
1345
|
+
"yarn",
|
|
1346
|
+
{
|
|
1347
|
+
options: YARN,
|
|
1348
|
+
operand: "command",
|
|
1349
|
+
subcommands: new Map([
|
|
1350
|
+
["dlx", { options: YARN_DLX, operand: "command", doc: "https://yarnpkg.com/cli/dlx" }],
|
|
1351
|
+
]),
|
|
1352
|
+
doc: "https://yarnpkg.com/cli",
|
|
1353
|
+
},
|
|
1354
|
+
],
|
|
1355
|
+
["bunx", { options: BUNX, operand: "command", doc: "https://bun.com/docs/pm/bunx" }],
|
|
1356
|
+
[
|
|
1357
|
+
"pnpx",
|
|
1358
|
+
{
|
|
1359
|
+
options: PNPM_DLX_OPTIONS,
|
|
1360
|
+
operand: "command",
|
|
1361
|
+
lines: PNPM_LINES,
|
|
1362
|
+
doc: "https://pnpm.io/cli/dlx#pnpx",
|
|
1363
|
+
},
|
|
1364
|
+
],
|
|
1365
|
+
["c8", { options: C8, operand: "command", doc: "https://github.com/bcoe/c8#cli-options" }],
|
|
1366
|
+
["nyc", { options: NYC, operand: "command", doc: "https://github.com/istanbuljs/nyc#usage" }],
|
|
1367
|
+
// Its flags stand only before a `--`, so a form with one before no `--` caps.
|
|
1368
|
+
[
|
|
1369
|
+
"dotenv",
|
|
1370
|
+
{ options: DOTENV, operand: "command", doc: "https://github.com/entropitor/dotenv-cli#usage" },
|
|
1371
|
+
],
|
|
1372
|
+
[
|
|
1373
|
+
"env-cmd",
|
|
1374
|
+
{
|
|
1375
|
+
options: ENV_CMD,
|
|
1376
|
+
operand: "command",
|
|
1377
|
+
lines: new Set(["--use-shell"]),
|
|
1378
|
+
doc: "https://github.com/toddbluhm/env-cmd#-help",
|
|
1379
|
+
},
|
|
1380
|
+
],
|
|
1381
|
+
[
|
|
1382
|
+
"dotenvx",
|
|
1383
|
+
{
|
|
1384
|
+
options: DOTENVX,
|
|
1385
|
+
operand: "command",
|
|
1386
|
+
subcommands: new Map([
|
|
1387
|
+
[
|
|
1388
|
+
"run",
|
|
1389
|
+
{
|
|
1390
|
+
options: DOTENVX_RUN,
|
|
1391
|
+
operand: "command",
|
|
1392
|
+
ended: true,
|
|
1393
|
+
doc: "https://dotenvx.com/docs/advanced/run",
|
|
1394
|
+
},
|
|
1395
|
+
],
|
|
1396
|
+
]),
|
|
1397
|
+
doc: "https://dotenvx.com/docs",
|
|
1398
|
+
},
|
|
1399
|
+
],
|
|
1400
|
+
...["sh", "bash", "dash", "zsh", "ksh", "ash"].map((name) => [
|
|
1401
|
+
name,
|
|
1402
|
+
{
|
|
1403
|
+
options: SHELL_OPTIONS,
|
|
1404
|
+
operand: "line",
|
|
1405
|
+
lines: SHELL_LINES,
|
|
1406
|
+
doc: "https://pubs.opengroup.org/onlinepubs/9799919799/utilities/sh.html",
|
|
1407
|
+
},
|
|
1408
|
+
]),
|
|
1409
|
+
[
|
|
1410
|
+
"concurrently",
|
|
1411
|
+
{
|
|
1412
|
+
options: CONCURRENTLY,
|
|
1413
|
+
operand: "lines",
|
|
1414
|
+
lines: new Set([TEARDOWN]),
|
|
1415
|
+
doc: "https://github.com/open-cli-tools/concurrently/blob/main/bin/index.ts",
|
|
1416
|
+
},
|
|
1417
|
+
],
|
|
1418
|
+
[
|
|
1419
|
+
"xargs",
|
|
1420
|
+
{
|
|
1421
|
+
options: XARGS,
|
|
1422
|
+
operand: "appended",
|
|
1423
|
+
doc: "https://www.gnu.org/software/findutils/manual/html_node/find_html/xargs-options.html",
|
|
1424
|
+
},
|
|
1425
|
+
],
|
|
1426
|
+
[
|
|
1427
|
+
"parallel",
|
|
1428
|
+
{
|
|
1429
|
+
options: PARALLEL,
|
|
1430
|
+
operand: "appended",
|
|
1431
|
+
doc: "https://www.gnu.org/software/parallel/parallel.html",
|
|
1432
|
+
},
|
|
1433
|
+
],
|
|
1434
|
+
]);
|
|
1435
|
+
/**
|
|
1436
|
+
* The row of each tool, by the name its program word's last segment writes, with its grammar,
|
|
1437
|
+
* which the enumerated property of `text/named-files.property.test.ts` spells every option of.
|
|
1438
|
+
*/
|
|
1439
|
+
export const TOOLS = new Map([...ROWS].map(([name, row]) => {
|
|
1440
|
+
const grammar = GRAMMARS.get(name);
|
|
1441
|
+
return [name, grammar === undefined ? row : { ...row, grammar }];
|
|
1442
|
+
}));
|
|
1443
|
+
/** Where the words after the program word of `row` write the command lines it hands to a shell. */
|
|
1444
|
+
function linesOf(row) {
|
|
1445
|
+
const grammar = row?.grammar;
|
|
1446
|
+
return (row?.launches?.lines ??
|
|
1447
|
+
(grammar?.lines === undefined || row?.launches === undefined
|
|
1448
|
+
? undefined
|
|
1449
|
+
: optionLines(grammar.options, grammar.lines, grammar.later)));
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* The row of a simple command's program word, by the name its last path segment writes, as a
|
|
1453
|
+
* package runner's operand where `packaged`, its version removed.
|
|
1454
|
+
*/
|
|
1455
|
+
function rowOf(program, packaged = false) {
|
|
1456
|
+
const name = programName(program)?.replace(/\.(?:exe|cmd)$/iu, "");
|
|
1457
|
+
return name === undefined ? undefined : TOOLS.get(commandName(name, packaged));
|
|
341
1458
|
}
|
|
342
1459
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
1460
|
+
* The last path segment of a program word, a dynamic one's too where it writes that segment
|
|
1461
|
+
* whole after a `/`, as `"$(npm root)/.bin/tsx"` does, or `undefined` where the run computes it.
|
|
345
1462
|
*/
|
|
1463
|
+
function programName(program) {
|
|
1464
|
+
const text = program?.value ?? program?.written;
|
|
1465
|
+
const last = text?.slice(text.lastIndexOf("/") + 1);
|
|
1466
|
+
return last === undefined ||
|
|
1467
|
+
last.includes(PART) ||
|
|
1468
|
+
(program?.value === undefined && last === text)
|
|
1469
|
+
? undefined
|
|
1470
|
+
: last;
|
|
1471
|
+
}
|
|
1472
|
+
/** What the reader of one simple command's program reads it as, or `undefined` where none understands it. */
|
|
346
1473
|
export function readTool(words) {
|
|
1474
|
+
const reading = readRow(words);
|
|
1475
|
+
return reading?.kind === "unknown" ? undefined : reading;
|
|
1476
|
+
}
|
|
1477
|
+
/** The command each launcher of `words` hands on to in turn, as {@link readTool} reads it, and what its last one is read as. */
|
|
1478
|
+
export function handedOn(words) {
|
|
1479
|
+
let handed = words;
|
|
1480
|
+
let reading = readTool(handed);
|
|
1481
|
+
while (reading?.kind === "command") {
|
|
1482
|
+
handed = reading.words;
|
|
1483
|
+
reading = readTool(handed);
|
|
1484
|
+
}
|
|
1485
|
+
return [handed, reading];
|
|
1486
|
+
}
|
|
1487
|
+
/** bash's `echo` options, which a word holds only, and a word holding any other letter is written: https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-echo */
|
|
1488
|
+
const ECHO_OPTION = /^-[neE]+$/u;
|
|
1489
|
+
/** The escapes of a `printf` format this reading writes, each the text it stands for; any other is unread. */
|
|
1490
|
+
const PRINTF_ESCAPES = new Map([
|
|
1491
|
+
["\\n", "\n"],
|
|
1492
|
+
["\\t", "\t"],
|
|
1493
|
+
["\\\\", "\\"],
|
|
1494
|
+
["%%", "%"],
|
|
1495
|
+
]);
|
|
1496
|
+
/**
|
|
1497
|
+
* What one simple command writes to its standard output, `undefined` where no row reads it,
|
|
1498
|
+
* another program's output among it. `echo` writes its operands joined by blanks as one line,
|
|
1499
|
+
* `undefined` where one holds a `\` or `-e` stands, as XSI echo reads escapes:
|
|
1500
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/echo.html. `printf` writes its
|
|
1501
|
+
* format, `undefined` where it holds a conversion or an escape other than `%%`, `\n`, `\t` and
|
|
1502
|
+
* `\\`, or other operands follow it, whose output POSIX leaves unspecified:
|
|
1503
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html. `cat` writes the file
|
|
1504
|
+
* each operand names and, with none or a `-`, its own standard input:
|
|
1505
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/cat.html.
|
|
1506
|
+
*/
|
|
1507
|
+
export function outputOf(words) {
|
|
1508
|
+
const name = programName(words[0]);
|
|
1509
|
+
const none = { at: [], joined: [], texts: [], input: false };
|
|
1510
|
+
if (name === "echo") {
|
|
1511
|
+
let from = 1;
|
|
1512
|
+
while (ECHO_OPTION.test(words[from]?.value ?? "")) {
|
|
1513
|
+
from += 1;
|
|
1514
|
+
}
|
|
1515
|
+
const escaped = words.slice(1, from).some((word) => word.value?.includes("e") === true) ||
|
|
1516
|
+
words.slice(from).some((word) => (word.value ?? word.written ?? word.text).includes("\\"));
|
|
1517
|
+
return escaped ? undefined : { ...none, joined: [[from, words.length]] };
|
|
1518
|
+
}
|
|
1519
|
+
if (name === "printf") {
|
|
1520
|
+
const operands = words.slice(words[1]?.value === "--" ? 2 : 1);
|
|
1521
|
+
const [format, ...more] = operands;
|
|
1522
|
+
const known = format?.value;
|
|
1523
|
+
if (known === undefined || known.startsWith("-") || more.length > 0) {
|
|
1524
|
+
return known === "-v" ? none : undefined;
|
|
1525
|
+
}
|
|
1526
|
+
const rest = known.replaceAll(/\\.|%./gsu, (escape) => PRINTF_ESCAPES.has(escape) ? "" : "\0");
|
|
1527
|
+
return rest.includes("\\") || rest.includes("%") || rest.includes("\0")
|
|
1528
|
+
? undefined
|
|
1529
|
+
: {
|
|
1530
|
+
...none,
|
|
1531
|
+
texts: [known.replaceAll(/\\.|%./gsu, (escape) => PRINTF_ESCAPES.get(escape) ?? escape)],
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
if (name !== "cat") {
|
|
1535
|
+
return undefined;
|
|
1536
|
+
}
|
|
1537
|
+
const operands = words.flatMap((word, at) => at > 0 && !/^-./u.test(word.value ?? "") ? [at] : []);
|
|
1538
|
+
return {
|
|
1539
|
+
...none,
|
|
1540
|
+
at: operands.filter((at) => words[at]?.value !== "-"),
|
|
1541
|
+
input: operands.length === 0 || operands.some((at) => words[at]?.value === "-"),
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
/** What the reader of one simple command's program reads it as, {@link UNKNOWN} where its table does not model it. */
|
|
1545
|
+
function readRow(words) {
|
|
347
1546
|
const [program, ...rest] = words;
|
|
348
|
-
return rowOf(program)?.read?.(rest);
|
|
1547
|
+
return after(1, rowOf(program)?.read?.(rest));
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* The test run of one simple command whose program word names a runtime, as `node --test`, the
|
|
1551
|
+
* one reading of its options that tests.1 roots its files by, or `undefined` where it runs no
|
|
1552
|
+
* test runner, its table does not model it or a word it needs is dynamic.
|
|
1553
|
+
*/
|
|
1554
|
+
export function testRun(words) {
|
|
1555
|
+
const [program, ...rest] = words;
|
|
1556
|
+
const line = rowOf(program)?.runtime;
|
|
1557
|
+
const read = line === undefined ? undefined : runtimeLine(line, rest);
|
|
1558
|
+
if (read === undefined || isUnknown(read) || read.tests === undefined) {
|
|
1559
|
+
return undefined;
|
|
1560
|
+
}
|
|
1561
|
+
const patterns = rest.slice(read.tests).map((word) => word.value);
|
|
1562
|
+
return patterns.every((one) => one !== undefined)
|
|
1563
|
+
? {
|
|
1564
|
+
patterns,
|
|
1565
|
+
stripped: !read.named.has("--no-strip-types") && !read.named.has("--no-experimental-strip-types"),
|
|
1566
|
+
}
|
|
1567
|
+
: undefined;
|
|
1568
|
+
}
|
|
1569
|
+
/** Whether `input`, the file a `<` names, writes no path, so a reader of it runs a file no word writes. */
|
|
1570
|
+
function unnamed(input) {
|
|
1571
|
+
return input !== undefined && input.value === undefined && !writesPath(input.written);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* The launch of the program word `program`, at `at` of `words`, where it may run a command
|
|
1575
|
+
* written after it: after one of its subcommands, past its own options, or a dynamic word or an
|
|
1576
|
+
* unread option that may be one, and for a shell where its words write a command line.
|
|
1577
|
+
*/
|
|
1578
|
+
function launchAt(words, at, program) {
|
|
1579
|
+
const launch = rowOf(program)?.launches;
|
|
1580
|
+
if (launch?.after !== undefined) {
|
|
1581
|
+
const sub = launch.options === undefined ? 0 : operandAt(words.slice(at + 1), launch.options);
|
|
1582
|
+
const next = sub === undefined ? undefined : words[at + 1 + sub];
|
|
1583
|
+
return sub === undefined ||
|
|
1584
|
+
(next !== undefined && (next.value === undefined || launch.after.has(next.value)))
|
|
1585
|
+
? launch
|
|
1586
|
+
: undefined;
|
|
1587
|
+
}
|
|
1588
|
+
return launch?.line !== true || (launch.lines?.(words.slice(at + 1)).at.size ?? 0) > 0
|
|
1589
|
+
? launch
|
|
1590
|
+
: undefined;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* The program words of `words`, a simple command: the first word, the program word each
|
|
1594
|
+
* launcher hands a command on to, as its reader reads it or, for one that hands its commands
|
|
1595
|
+
* the paths it finds, where each starts after its options, and, after a launcher whose words
|
|
1596
|
+
* its reader does not read or that has no reader, each later word up to the first dynamic one.
|
|
1597
|
+
*/
|
|
1598
|
+
function chainOf(words) {
|
|
1599
|
+
const read = [...words];
|
|
1600
|
+
const replaced = new Map();
|
|
1601
|
+
const programs = new Map();
|
|
1602
|
+
let dynamic;
|
|
1603
|
+
let end;
|
|
1604
|
+
let unread;
|
|
1605
|
+
let unknown = false;
|
|
1606
|
+
const scanned = new Set();
|
|
1607
|
+
const queue = [[0, read[0]]];
|
|
1608
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
1609
|
+
const [at, program] = next;
|
|
1610
|
+
if (program === undefined || programs.has(at)) {
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (programName(program) === undefined) {
|
|
1614
|
+
dynamic = Math.min(dynamic ?? at, at);
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
programs.set(at, program);
|
|
1618
|
+
const launch = launchAt(read, at, program);
|
|
1619
|
+
const appending = launch?.appends?.(read.slice(at + 1));
|
|
1620
|
+
const pattern = appending?.replaced;
|
|
1621
|
+
for (let later = at + 1 + (appending?.from ?? read.length); pattern !== undefined && later < read.length; later += 1) {
|
|
1622
|
+
const word = read[later];
|
|
1623
|
+
const written = word?.written?.replace(pattern, PART);
|
|
1624
|
+
replaced.set(later, pattern);
|
|
1625
|
+
if (word !== undefined && written !== undefined && written !== word.written) {
|
|
1626
|
+
read[later] = { value: undefined, text: word.text, written };
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (appending?.end !== undefined) {
|
|
1630
|
+
end = Math.min(end ?? read.length, at + 1 + appending.end);
|
|
1631
|
+
}
|
|
1632
|
+
if (appending !== undefined && appending.starts === undefined) {
|
|
1633
|
+
unread = Math.min(unread ?? at, at);
|
|
1634
|
+
}
|
|
1635
|
+
const reading = launch === undefined || appending !== undefined
|
|
1636
|
+
? undefined
|
|
1637
|
+
: readRow([program, ...read.slice(at + 1)]);
|
|
1638
|
+
unknown ||= reading?.kind === "unknown";
|
|
1639
|
+
if (appending?.starts !== undefined) {
|
|
1640
|
+
queue.push(...appending.starts.map((start) => [at + 1 + start, read[at + 1 + start]]));
|
|
1641
|
+
}
|
|
1642
|
+
else if (reading?.kind === "command") {
|
|
1643
|
+
queue.push([at + reading.at, reading.words[0]]);
|
|
1644
|
+
}
|
|
1645
|
+
else if (launch !== undefined && (reading === undefined || reading.kind === "unknown")) {
|
|
1646
|
+
// An earlier scan that reached `later` queued every word from there to the same stop.
|
|
1647
|
+
for (let later = at + 1; later < read.length && !scanned.has(later); later += 1) {
|
|
1648
|
+
scanned.add(later);
|
|
1649
|
+
queue.push([later, read[later]]);
|
|
1650
|
+
if (programName(read[later]) === undefined) {
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return { programs, dynamic, words: read, replaced, end, unread, unknown };
|
|
1657
|
+
}
|
|
1658
|
+
/** A character a glob reads specially, so a pattern holding one names more than the path it writes. */
|
|
1659
|
+
const GLOB = /[*?[\]{}]/u;
|
|
1660
|
+
/**
|
|
1661
|
+
* The words of one simple command that may write the file a runtime runs (roots.13): where
|
|
1662
|
+
* `program` holds, a dynamic program word and every word after it; after each program word
|
|
1663
|
+
* {@link chainOf} places that names a runtime, its entry, or every later word its reader does
|
|
1664
|
+
* not read. A runtime with no entry after a tool that hands it the paths it finds, a command a
|
|
1665
|
+
* row's table does not model, or one that reads command lines or its standard input from the
|
|
1666
|
+
* file `input`, which a `<` names with no path, runs a file no word writes. `tests` lets tests.1 root this command's own test run; otherwise a test runner's
|
|
1667
|
+
* pattern may write a file, and a dynamic or glob one, or none, runs a file no word writes.
|
|
1668
|
+
*/
|
|
1669
|
+
export function fileOperands(words, program, input, tests = false) {
|
|
1670
|
+
const chain = chainOf(words);
|
|
1671
|
+
const read = chain.words;
|
|
1672
|
+
const places = new Set();
|
|
1673
|
+
const fromHere = (at) => {
|
|
1674
|
+
for (let later = at; later < read.length; later += 1) {
|
|
1675
|
+
places.add(later);
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
if (program && chain.dynamic !== undefined) {
|
|
1679
|
+
fromHere(chain.dynamic);
|
|
1680
|
+
}
|
|
1681
|
+
let unwritten = chain.unknown || chain.unread !== undefined;
|
|
1682
|
+
let reads = false;
|
|
1683
|
+
let stdin = false;
|
|
1684
|
+
const programs = [...chain.programs].sort(([one], [other]) => one - other);
|
|
1685
|
+
const [handed] = tests ? handedOn(words) : [[]];
|
|
1686
|
+
// Where the command reader's own test run is this runtime's, tests.1 roots its files.
|
|
1687
|
+
const rooted = testRun(handed) !== undefined &&
|
|
1688
|
+
handed.every((word, at) => read[read.length - handed.length + at]?.value === word.value)
|
|
1689
|
+
? read.length - handed.length
|
|
1690
|
+
: -1;
|
|
1691
|
+
const appender = programs.find(([, word]) => rowOf(word)?.launches?.appends !== undefined)?.[0] ?? -1;
|
|
1692
|
+
for (const [named, word] of programs) {
|
|
1693
|
+
const line = rowOf(word, true)?.runtime;
|
|
1694
|
+
if (line === undefined) {
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
const end = chain.end !== undefined && named < chain.end ? chain.end : read.length;
|
|
1698
|
+
const runtime = runtimeLine(line, read.slice(named + 1, end));
|
|
1699
|
+
if (runtime === undefined || isUnknown(runtime)) {
|
|
1700
|
+
fromHere(named + 1);
|
|
1701
|
+
unwritten ||= isUnknown(runtime);
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
stdin ||= runtime.stdin;
|
|
1705
|
+
if (runtime.tests !== undefined && named !== rooted) {
|
|
1706
|
+
const from = named + 1 + runtime.tests;
|
|
1707
|
+
const patterns = read.slice(from, end);
|
|
1708
|
+
patterns.forEach((_, at) => places.add(from + at));
|
|
1709
|
+
unwritten ||=
|
|
1710
|
+
patterns.length === 0 ||
|
|
1711
|
+
patterns.some((one) => one.value === undefined || GLOB.test(one.value));
|
|
1712
|
+
}
|
|
1713
|
+
if (runtime.entry === undefined) {
|
|
1714
|
+
unwritten ||= appender >= 0 && appender < named;
|
|
1715
|
+
}
|
|
1716
|
+
else {
|
|
1717
|
+
places.add(named + 1 + runtime.entry);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const lines = new Map();
|
|
1721
|
+
const replaced = new Map(chain.replaced);
|
|
1722
|
+
const joined = [];
|
|
1723
|
+
const texts = [];
|
|
1724
|
+
for (const [at, word] of programs) {
|
|
1725
|
+
const found = linesOf(rowOf(word))?.(read.slice(at + 1)) ?? NO_LINES;
|
|
1726
|
+
for (const [one, prefix] of found.at) {
|
|
1727
|
+
lines.set(at + 1 + one, prefix);
|
|
1728
|
+
if (found.replaced !== undefined) {
|
|
1729
|
+
replaced.set(at + 1 + one, found.replaced);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
joined.push(...(found.joined ?? []).map(([from, to]) => [at + 1 + from, at + 1 + to]));
|
|
1733
|
+
texts.push(...(found.texts ?? []));
|
|
1734
|
+
reads ||= found.input;
|
|
1735
|
+
unwritten ||=
|
|
1736
|
+
found.unwritten === true || (found.appended === true && appender >= 0 && appender < at);
|
|
1737
|
+
}
|
|
1738
|
+
unwritten ||= (reads || stdin) && unnamed(input);
|
|
1739
|
+
return { places, unwritten, lines, input: reads, stdin, replaced, joined, texts };
|
|
349
1740
|
}
|
|
350
1741
|
/**
|
|
351
1742
|
* The configuration files one simple command names, as written: each static value of an option
|