@cplieger/deadset-ts 8.0.0-dev.18 → 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 +4 -1
- 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 +7 -0
- 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/findings/non-code-artifacts.js +11 -5
- package/dist/src/roots/manifest.js +47 -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/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +13 -1
- 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 +7 -0
- 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/file-facts.ts +5 -0
- package/src/findings/non-code-artifacts.ts +16 -5
- package/src/roots/manifest.ts +53 -8
- package/src/roots/root-set.ts +18 -16
- package/src/text/named-files.ts +333 -36
- package/src/verbs/analyze.ts +16 -1
package/src/commands/tools.ts
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
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
|
|
|
9
10
|
import { hasURLScheme, isPathSpecifier, resolvePath } from "../paths.ts";
|
|
10
|
-
import type
|
|
11
|
+
import { PART, type Word } from "./shell.ts";
|
|
11
12
|
|
|
12
13
|
/** One module a runtime loads before its entry, and the resolution its option names it by. */
|
|
13
14
|
export interface Preload {
|
|
@@ -26,70 +27,219 @@ export type Reading =
|
|
|
26
27
|
/** `node` where the entry runs under Node itself. */
|
|
27
28
|
readonly runtime: "node" | undefined;
|
|
28
29
|
}
|
|
29
|
-
/** It runs the command `words
|
|
30
|
-
| { readonly kind: "command"; readonly words: readonly Word[] }
|
|
30
|
+
/** It runs the command `words`, whose program word stands at `at` of the words read, as read. */
|
|
31
|
+
| { readonly kind: "command"; readonly words: readonly Word[]; readonly at: number }
|
|
31
32
|
/** It runs `text` as a command line of its own. */
|
|
32
33
|
| { readonly kind: "line"; readonly text: string };
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* A reader's answer where its tool's table does not model the command: an option the table
|
|
37
|
+
* does not list, or a construct no row reads. Layer 2 reads it as not understood; for layer 3
|
|
38
|
+
* the command may run a file no word writes, and every later word may still be a program word.
|
|
39
|
+
*/
|
|
40
|
+
interface Unknown {
|
|
41
|
+
readonly kind: "unknown";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const UNKNOWN: Unknown = { kind: "unknown" };
|
|
45
|
+
|
|
46
|
+
function isUnknown(value: unknown): value is Unknown {
|
|
47
|
+
return value === UNKNOWN;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Reader = (words: readonly Word[]) => Reading | Unknown | undefined;
|
|
35
51
|
|
|
36
52
|
/** The options a tool takes before its first operand. */
|
|
37
|
-
interface Options {
|
|
53
|
+
export interface Options {
|
|
38
54
|
/** The options that take a value, the next word or, for a long one, the text after `=`. */
|
|
39
55
|
readonly valued: ReadonlySet<string>;
|
|
40
56
|
/** The options that take none. */
|
|
41
57
|
readonly flags: ReadonlySet<string>;
|
|
42
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Whether its one-letter options read as getopt reads them: a valued one's value may stand in
|
|
60
|
+
* the same word, as `-uNAME`, and several may share one word, the last of them valued or not,
|
|
61
|
+
* as `-iS'…'` and `-iu NAME`: https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/V1_chap12.html
|
|
62
|
+
*/
|
|
43
63
|
readonly attached?: boolean;
|
|
64
|
+
/** The options whose value is optional and only written in the same word, as xargs's `-i[R]`. */
|
|
65
|
+
readonly optional?: ReadonlySet<string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One tool's options as its documentation gives them: its `options`, the object its reader
|
|
70
|
+
* parses with; what follows them, a command, the file it runs, a command line, command lines
|
|
71
|
+
* among its options, or the command it hands the paths it finds; the options that give a
|
|
72
|
+
* command line, a valued one's value or, for a flag, its operands; its subcommands, each with
|
|
73
|
+
* its own table; and whether it reads options after its first operand, up to a `--`.
|
|
74
|
+
* Production reads `options`, `lines` and `later` only, through {@link linesOf}; the
|
|
75
|
+
* enumerated property spells every field.
|
|
76
|
+
*/
|
|
77
|
+
export interface Grammar {
|
|
78
|
+
readonly options: Options;
|
|
79
|
+
readonly operand: "command" | "file" | "line" | "lines" | "appended";
|
|
80
|
+
readonly lines?: ReadonlySet<string>;
|
|
81
|
+
readonly subcommands?: ReadonlyMap<string, Grammar>;
|
|
82
|
+
readonly later?: boolean;
|
|
83
|
+
/** Whether its command stands only after a `--`, as `dotenvx run`'s. */
|
|
84
|
+
readonly ended?: boolean;
|
|
85
|
+
/** For a runtime, the options that run their value, or its test runner, in its file's place. */
|
|
86
|
+
readonly inline?: ReadonlySet<string>;
|
|
87
|
+
/** For a runtime, the words that pick a mode that runs no file, and the options that may stand before one. */
|
|
88
|
+
readonly modes?: ReadonlySet<string>;
|
|
89
|
+
readonly modeAfter?: ReadonlySet<string>;
|
|
90
|
+
readonly doc: string;
|
|
44
91
|
}
|
|
45
92
|
|
|
46
93
|
/** The options before the first operand, with their values, and the operand's index. */
|
|
47
94
|
interface Read {
|
|
48
95
|
readonly values: ReadonlyMap<string, readonly string[]>;
|
|
96
|
+
/** The name of each option read, a flag's too. */
|
|
97
|
+
readonly named: ReadonlySet<string>;
|
|
49
98
|
readonly at: number;
|
|
50
99
|
/** Whether a `--` ended the options. */
|
|
51
100
|
readonly ended: boolean;
|
|
52
101
|
}
|
|
53
102
|
|
|
54
|
-
/** The
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
/** The text `word` writes before its first part only the run knows: all of it where it is static. */
|
|
104
|
+
function startOf(word: Word): string {
|
|
105
|
+
return word.value ?? word.written?.split(PART)[0] ?? "";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One option {@link scanOptions} reads, with where its value is written: in the word at `at`, after `prefix`. */
|
|
109
|
+
interface Scanned {
|
|
110
|
+
readonly name: string;
|
|
111
|
+
readonly value?: { readonly at: number; readonly prefix: string };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The options {@link scanOptions} reads, and where they end: at an operand, after a `--`, at
|
|
116
|
+
* the words' end, at an option `options` does not hold, or at a dynamic word that may be one.
|
|
117
|
+
*/
|
|
118
|
+
interface Scan {
|
|
119
|
+
readonly options: readonly Scanned[];
|
|
120
|
+
readonly at: number;
|
|
121
|
+
readonly stop: "operand" | "ended" | "end" | "unread" | "dynamic";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The options one word `word`, starting with `-`, writes, the word at `at`, or `undefined` where
|
|
126
|
+
* `options` does not hold one. A valued one's value is the next word's unless the word writes
|
|
127
|
+
* it, and always where it is `dynamic`, whose written text goes on past `word`.
|
|
128
|
+
*/
|
|
129
|
+
function optionsInWord(
|
|
130
|
+
word: string,
|
|
131
|
+
at: number,
|
|
132
|
+
options: Options,
|
|
133
|
+
dynamic: boolean,
|
|
134
|
+
): readonly Scanned[] | undefined {
|
|
135
|
+
const inWord = (name: string, prefix: string): Scanned => ({ name, value: { at, prefix } });
|
|
136
|
+
if (word.startsWith("--")) {
|
|
137
|
+
const equals = word.indexOf("=");
|
|
138
|
+
const name = equals < 0 ? word : word.slice(0, equals);
|
|
139
|
+
if (equals >= 0) {
|
|
140
|
+
return options.valued.has(name) || options.optional?.has(name) === true
|
|
141
|
+
? [inWord(name, `${name}=`)]
|
|
142
|
+
: undefined;
|
|
143
|
+
}
|
|
144
|
+
if (options.optional?.has(word) === true) {
|
|
145
|
+
return [inWord(word, word)];
|
|
146
|
+
}
|
|
147
|
+
return options.valued.has(word) && !dynamic
|
|
148
|
+
? [{ name: word, value: { at: at + 1, prefix: "" } }]
|
|
149
|
+
: undefined;
|
|
150
|
+
}
|
|
151
|
+
if (!dynamic && options.valued.has(word)) {
|
|
152
|
+
return [{ name: word, value: { at: at + 1, prefix: "" } }];
|
|
153
|
+
}
|
|
154
|
+
if (options.attached !== true) {
|
|
155
|
+
return options.optional?.has(word) === true ? [inWord(word, word)] : undefined;
|
|
156
|
+
}
|
|
157
|
+
const found: Scanned[] = [];
|
|
158
|
+
for (let letter = 1; letter < word.length; letter += 1) {
|
|
159
|
+
const name = `-${word.charAt(letter)}`;
|
|
160
|
+
const prefix = word.slice(0, letter + 1);
|
|
161
|
+
if (options.valued.has(name)) {
|
|
162
|
+
const next = letter + 1 === word.length && !dynamic;
|
|
163
|
+
return [...found, next ? { name, value: { at: at + 1, prefix: "" } } : inWord(name, prefix)];
|
|
164
|
+
}
|
|
165
|
+
if (options.optional?.has(name) === true) {
|
|
166
|
+
return [...found, inWord(name, prefix)];
|
|
167
|
+
}
|
|
168
|
+
if (!options.flags.has(name)) {
|
|
63
169
|
return undefined;
|
|
64
170
|
}
|
|
65
|
-
|
|
66
|
-
|
|
171
|
+
found.push({ name });
|
|
172
|
+
}
|
|
173
|
+
return found.length === 0 || dynamic ? undefined : found;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The options `words` write from `from` on, read by `options`, the one reading of a table's
|
|
178
|
+
* options that {@link optionsOf}, {@link optionLines} and {@link envSplit} share. A dynamic
|
|
179
|
+
* word that starts with written text other than `-` is no option; one whose written start is a
|
|
180
|
+
* valued option's ends them with that option's value in it, as `-S"$CMD"` does.
|
|
181
|
+
*/
|
|
182
|
+
function scanOptions(words: readonly Word[], options: Options, from = 0): Scan {
|
|
183
|
+
const found: Scanned[] = [];
|
|
184
|
+
for (let at = from; at < words.length; at += 1) {
|
|
185
|
+
const one = words[at];
|
|
186
|
+
const dynamic = one?.value === undefined;
|
|
187
|
+
const word = one === undefined ? "" : startOf(one);
|
|
188
|
+
if (!dynamic && word === "--") {
|
|
189
|
+
return { options: found, at: at + 1, stop: "ended" };
|
|
67
190
|
}
|
|
68
|
-
if (!
|
|
69
|
-
|
|
70
|
-
return { values, at, ended: false };
|
|
71
|
-
}
|
|
191
|
+
if (!dynamic && options.flags.has(word)) {
|
|
192
|
+
found.push({ name: word });
|
|
72
193
|
continue;
|
|
73
194
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
195
|
+
if (!word.startsWith("-")) {
|
|
196
|
+
return { options: found, at, stop: dynamic && word === "" ? "dynamic" : "operand" };
|
|
197
|
+
}
|
|
198
|
+
const read = optionsInWord(word, at, options, dynamic);
|
|
199
|
+
if (dynamic || read === undefined) {
|
|
200
|
+
return {
|
|
201
|
+
options: [...found, ...(read ?? [])],
|
|
202
|
+
at,
|
|
203
|
+
stop: dynamic ? "dynamic" : "unread",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
found.push(...read);
|
|
207
|
+
at = read.at(-1)?.value?.at ?? at;
|
|
208
|
+
}
|
|
209
|
+
return { options: found, at: words.length, stop: "end" };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The options `words` start with, {@link UNKNOWN} where `options` does not hold one, or
|
|
214
|
+
* `undefined` where a needed word is dynamic.
|
|
215
|
+
*/
|
|
216
|
+
function optionsOf(words: readonly Word[], options: Options): Read | Unknown | undefined {
|
|
217
|
+
const scan = scanOptions(words, options);
|
|
218
|
+
if (scan.stop === "unread") {
|
|
219
|
+
return UNKNOWN;
|
|
220
|
+
}
|
|
221
|
+
if (scan.stop === "dynamic") {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const values = new Map<string, string[]>();
|
|
225
|
+
const named = new Set<string>();
|
|
226
|
+
for (const { name, value } of scan.options) {
|
|
227
|
+
named.add(name);
|
|
228
|
+
const text = value === undefined ? undefined : words[value.at]?.value;
|
|
229
|
+
if (value !== undefined && text === undefined) {
|
|
89
230
|
return undefined;
|
|
90
231
|
}
|
|
232
|
+
if (value !== undefined && text !== undefined) {
|
|
233
|
+
values.set(name, [...(values.get(name) ?? []), text.slice(value.prefix.length)]);
|
|
234
|
+
}
|
|
91
235
|
}
|
|
92
|
-
return { values, at:
|
|
236
|
+
return { values, named, at: scan.at, ended: scan.stop === "ended" };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The place of the first operand after the options `words` start with, `undefined` where {@link optionsOf} reads none. */
|
|
240
|
+
function operandAt(words: readonly Word[], options: Options): number | undefined {
|
|
241
|
+
const read = optionsOf(words, options);
|
|
242
|
+
return read === undefined || isUnknown(read) ? undefined : read.at;
|
|
93
243
|
}
|
|
94
244
|
|
|
95
245
|
/**
|
|
@@ -140,77 +290,255 @@ export function preloadTarget(
|
|
|
140
290
|
return path === undefined ? undefined : { path: resolvePath(dir, path) };
|
|
141
291
|
}
|
|
142
292
|
|
|
143
|
-
/** A runtime: its preload options, then its entry, and every later word handed to the program. */
|
|
144
|
-
|
|
145
|
-
preloads: ReadonlyMap<string, Preload["resolution"]
|
|
146
|
-
flags: readonly string[]
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
293
|
+
/** A runtime's command line: its preload options, then its entry, and every later word handed to the program. */
|
|
294
|
+
interface Runtime {
|
|
295
|
+
readonly preloads: ReadonlyMap<string, Preload["resolution"]>;
|
|
296
|
+
readonly flags: readonly string[];
|
|
297
|
+
/** The other options that take a value, the next word or the text after `=`. */
|
|
298
|
+
readonly valued?: readonly string[];
|
|
299
|
+
/** The options whose value is only written after `=`, as V8's `--stack-size=`. */
|
|
300
|
+
readonly optional?: readonly string[];
|
|
301
|
+
/** Whether the entry runs under Node itself. */
|
|
302
|
+
readonly runsNode: boolean;
|
|
303
|
+
/** The words that pick a mode that runs the entry, after its options or before them, as tsx's `watch`. */
|
|
304
|
+
readonly subcommands?: ReadonlySet<string>;
|
|
305
|
+
/** The words that pick a mode that runs no entry, as `bun install`. */
|
|
306
|
+
readonly modes?: ReadonlySet<string>;
|
|
307
|
+
/** The only options that may stand before a word that picks a mode, where not every one may. */
|
|
308
|
+
readonly modeAfter?: ReadonlySet<string>;
|
|
309
|
+
/** The options whose value it runs in its entry's place, as Node's `--eval`. */
|
|
310
|
+
readonly inline?: readonly string[];
|
|
311
|
+
/** Whether it runs its standard input as its entry where none is written, as Node does. */
|
|
312
|
+
readonly stdin?: boolean;
|
|
313
|
+
/** The options that run its test runner in its entry's place, whose files `entries/test-runners.ts` reads. */
|
|
314
|
+
readonly runner?: readonly string[];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Where a runtime's words, those after its program word, write its entry, and what it preloads. */
|
|
318
|
+
interface RuntimeLine {
|
|
319
|
+
/** The entry's place among the words, `undefined` where none is written. */
|
|
320
|
+
readonly entry: number | undefined;
|
|
321
|
+
readonly preloads: readonly Preload[];
|
|
322
|
+
/** Whether it runs its standard input in its entry's place. */
|
|
323
|
+
readonly stdin: boolean;
|
|
324
|
+
/** Where an option runs its test runner, the place of the first word naming the files it runs. */
|
|
325
|
+
readonly tests: number | undefined;
|
|
326
|
+
/** The name of each option read. */
|
|
327
|
+
readonly named: ReadonlySet<string>;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The options of a runtime's command line, as {@link optionsOf} reads them. */
|
|
331
|
+
function runtimeOptions(line: Runtime): Options {
|
|
332
|
+
return {
|
|
333
|
+
valued: new Set([...line.preloads.keys(), ...(line.inline ?? []), ...(line.valued ?? [])]),
|
|
334
|
+
flags: new Set([...line.flags, ...(line.stdin === true ? ["-"] : [])]),
|
|
335
|
+
optional: new Set(line.optional),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The entries that name the runtime's own standard input: https://man7.org/linux/man-pages/man4/fd.4.html */
|
|
340
|
+
const STDIN_PATHS: ReadonlySet<string> = new Set(["/dev/stdin", "/dev/fd/0"]);
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* How a runtime reads its words, those after its program word, {@link UNKNOWN} where one of
|
|
344
|
+
* its options is unhandled, or `undefined` where a word it needs is dynamic: the one reading
|
|
345
|
+
* layer 2 runs a runtime by and layer 3 finds the word that may write its entry by.
|
|
346
|
+
*/
|
|
347
|
+
function runtimeLine(line: Runtime, words: readonly Word[]): RuntimeLine | Unknown | undefined {
|
|
348
|
+
const options = runtimeOptions(line);
|
|
349
|
+
const before = optionsOf(words, options);
|
|
350
|
+
if (before === undefined || isUnknown(before)) {
|
|
351
|
+
return before;
|
|
352
|
+
}
|
|
353
|
+
const picked = words[before.at]?.value;
|
|
354
|
+
const modal =
|
|
355
|
+
line.modes?.has(picked ?? "") === true &&
|
|
356
|
+
words
|
|
357
|
+
.slice(0, before.at)
|
|
358
|
+
.every((word) => line.modeAfter === undefined || line.modeAfter.has(word.value ?? ""));
|
|
359
|
+
const from = picked !== undefined && line.subcommands?.has(picked) === true ? before.at + 1 : 0;
|
|
360
|
+
const read = from === 0 ? before : optionsOf(words.slice(from), options);
|
|
361
|
+
if (read === undefined || isUnknown(read)) {
|
|
362
|
+
return read;
|
|
363
|
+
}
|
|
364
|
+
const values = (name: string): readonly string[] => [
|
|
365
|
+
...(from === 0 ? [] : (before.values.get(name) ?? [])),
|
|
366
|
+
...(read.values.get(name) ?? []),
|
|
367
|
+
];
|
|
368
|
+
const named = new Set([...(from === 0 ? [] : before.named), ...read.named]);
|
|
369
|
+
const runner = (line.runner ?? []).some((name) => named.has(name));
|
|
370
|
+
const inline = runner || (line.inline ?? []).some((name) => values(name).length > 0);
|
|
371
|
+
// `-` runs its standard input, and hands every later word to it.
|
|
372
|
+
const dash = words.slice(0, from + read.at).some((word) => word.value === "-");
|
|
373
|
+
const entry = dash ? words.length : from + read.at;
|
|
374
|
+
const piped = STDIN_PATHS.has(words[entry]?.value ?? "");
|
|
375
|
+
return {
|
|
376
|
+
entry: inline || piped || entry >= words.length || (from === 0 && modal) ? undefined : entry,
|
|
377
|
+
stdin: !inline && (piped || (line.stdin === true && entry >= words.length)),
|
|
378
|
+
tests: runner ? from + read.at : undefined,
|
|
379
|
+
named,
|
|
380
|
+
preloads: [...line.preloads].flatMap(([name, resolution]) =>
|
|
381
|
+
values(name).map((specifier) => ({ specifier, resolution })),
|
|
382
|
+
),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** The reader of a runtime's command line. */
|
|
387
|
+
function runtime(line: Runtime): Reader {
|
|
151
388
|
return (words) => {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return undefined;
|
|
389
|
+
const read = runtimeLine(line, words);
|
|
390
|
+
if (isUnknown(read)) {
|
|
391
|
+
return UNKNOWN;
|
|
156
392
|
}
|
|
157
|
-
const entry = words[
|
|
158
|
-
const loads = [...preloads].flatMap(([name, resolution]) =>
|
|
159
|
-
(read.values.get(name) ?? []).map((specifier) => ({ specifier, resolution })),
|
|
160
|
-
);
|
|
393
|
+
const entry = read?.entry === undefined ? undefined : words[read.entry];
|
|
161
394
|
if (
|
|
395
|
+
read === undefined ||
|
|
396
|
+
(read.stdin && words.some((word) => word.value === "-")) ||
|
|
162
397
|
(entry !== undefined && entry.value === undefined) ||
|
|
163
|
-
|
|
398
|
+
read.preloads.some((one) => preloadTarget(one, "/") === undefined)
|
|
164
399
|
) {
|
|
165
400
|
return undefined;
|
|
166
401
|
}
|
|
167
402
|
return {
|
|
168
403
|
kind: "run",
|
|
169
404
|
entry: entry?.value,
|
|
170
|
-
preloads:
|
|
171
|
-
runtime: runsNode && entry !== undefined ? "node" : undefined,
|
|
405
|
+
preloads: read.preloads,
|
|
406
|
+
runtime: line.runsNode && entry !== undefined ? "node" : undefined,
|
|
172
407
|
};
|
|
173
408
|
};
|
|
174
409
|
}
|
|
175
410
|
|
|
411
|
+
/** The options that run their value, code or a script's name, in a runtime's entry's place. */
|
|
412
|
+
const INLINE = ["-e", "--eval", "-p", "--print"];
|
|
413
|
+
|
|
176
414
|
/**
|
|
177
415
|
* `node [options] [entry] [arguments]`, https://nodejs.org/api/cli.html: the options that load
|
|
178
|
-
* a module before the entry, `-r` as `require` does and the rest as an ECMAScript module
|
|
179
|
-
*
|
|
416
|
+
* a module before the entry, `-r` as `require` does and the rest as an ECMAScript module;
|
|
417
|
+
* `--run`, which runs a script of the manifest; and the options that leave the entry where it
|
|
418
|
+
* is, `--inspect` and its kin and V8's taking their value only after `=`. With no entry, or
|
|
419
|
+
* `-`, it runs its standard input, which the second layer does not read, but where `--test`
|
|
420
|
+
* runs its test runner on the files its operands, or its default patterns, name.
|
|
180
421
|
*/
|
|
181
|
-
const
|
|
182
|
-
new Map([
|
|
422
|
+
const NODE: Runtime = {
|
|
423
|
+
preloads: new Map([
|
|
183
424
|
["-r", "require"],
|
|
184
425
|
["--require", "require"],
|
|
185
426
|
["--import", "import"],
|
|
186
427
|
["--loader", "import"],
|
|
187
428
|
["--experimental-loader", "import"],
|
|
188
429
|
]),
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
430
|
+
flags: long(
|
|
431
|
+
...["no-warnings", "no-deprecation", "enable-source-maps", "experimental-vm-modules"],
|
|
432
|
+
...["experimental-strip-types", "no-experimental-strip-types", "experimental-transform-types"],
|
|
433
|
+
...["experimental-detect-module", "no-experimental-detect-module"],
|
|
434
|
+
...["experimental-require-module", "no-experimental-require-module", "trace-warnings"],
|
|
435
|
+
...["trace-deprecation", "trace-uncaught", "throw-deprecation", "pending-deprecation"],
|
|
436
|
+
...["expose-gc", "preserve-symlinks", "preserve-symlinks-main", "frozen-intrinsics"],
|
|
437
|
+
...["abort-on-uncaught-exception", "watch", "watch-preserve-output", "jitless"],
|
|
438
|
+
...["openssl-legacy-provider", "use-openssl-ca", "use-system-ca", "use-bundled-ca"],
|
|
439
|
+
...["no-addons", "insecure-http-parser", "version", "help", "v8-options", "test"],
|
|
440
|
+
...["test-only", "test-force-exit", "test-update-snapshots", "test-randomize"],
|
|
441
|
+
...["experimental-test-coverage", "experimental-test-module-mocks", "no-strip-types"],
|
|
442
|
+
...["experimental-test-snapshots"],
|
|
443
|
+
).concat(["-v", "-h"]),
|
|
444
|
+
valued: [
|
|
445
|
+
"-C",
|
|
446
|
+
...long("conditions", "input-type", "env-file", "env-file-if-exists", "title"),
|
|
447
|
+
...long("disable-warning", "unhandled-rejections", "watch-path", "dns-result-order"),
|
|
448
|
+
...long("icu-data-dir", "redirect-warnings", "diagnostic-dir", "max-http-header-size"),
|
|
449
|
+
...long("test-reporter", "test-reporter-destination", "test-concurrency", "test-timeout"),
|
|
450
|
+
...long("test-name-pattern", "test-skip-pattern", "test-shard", "test-isolation"),
|
|
451
|
+
...long("test-coverage-include", "test-coverage-exclude", "test-coverage-branches"),
|
|
452
|
+
...long("test-coverage-functions", "test-coverage-lines", "test-global-setup"),
|
|
453
|
+
],
|
|
454
|
+
optional: long("inspect", "inspect-brk", "inspect-wait", "max-old-space-size", "stack-size"),
|
|
455
|
+
runsNode: true,
|
|
456
|
+
inline: [...INLINE, "--run"],
|
|
457
|
+
stdin: true,
|
|
458
|
+
runner: ["--test"],
|
|
459
|
+
};
|
|
192
460
|
|
|
193
|
-
/**
|
|
194
|
-
|
|
195
|
-
|
|
461
|
+
/**
|
|
462
|
+
* `tsx [watch] [options] <file> [arguments]`, https://tsx.is/usage, which takes Node's options
|
|
463
|
+
* too: https://tsx.is/node-enhancement
|
|
464
|
+
*/
|
|
465
|
+
const TSX: Runtime = {
|
|
466
|
+
preloads: new Map([
|
|
196
467
|
["-r", "require"],
|
|
197
468
|
["--require", "require"],
|
|
198
469
|
["--import", "import"],
|
|
199
470
|
]),
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
"
|
|
203
|
-
|
|
471
|
+
// `tsx --test` runs Node's test runner on files only `node --test` is read for, so it caps.
|
|
472
|
+
flags: [...NODE.flags.filter((name) => name !== "--test"), "--no-cache"],
|
|
473
|
+
valued: [...(NODE.valued ?? []), "--tsconfig"],
|
|
474
|
+
optional: NODE.optional ?? [],
|
|
475
|
+
runsNode: false,
|
|
476
|
+
subcommands: new Set(["watch"]),
|
|
477
|
+
stdin: true,
|
|
478
|
+
};
|
|
204
479
|
|
|
205
480
|
/** `ts-node [options] [script] [arguments]`, https://typestrong.org/ts-node/docs/options; `--esm` picks no file. */
|
|
206
|
-
const
|
|
207
|
-
new Map([
|
|
481
|
+
const TS_NODE: Runtime = {
|
|
482
|
+
preloads: new Map([
|
|
208
483
|
["-r", "require"],
|
|
209
484
|
["--require", "require"],
|
|
210
485
|
]),
|
|
211
|
-
["--esm"],
|
|
212
|
-
false,
|
|
213
|
-
|
|
486
|
+
flags: ["--esm", "-v", "--version", "-h", "--help"],
|
|
487
|
+
runsNode: false,
|
|
488
|
+
inline: INLINE,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* `bun [options] [run] [options] <file> [arguments]`, https://bun.com/docs/cli/run; its package
|
|
493
|
+
* manager, test runner, bundler and `bun x` subcommands run no entry.
|
|
494
|
+
*/
|
|
495
|
+
const BUN: Runtime = {
|
|
496
|
+
preloads: new Map([
|
|
497
|
+
["-r", "require"],
|
|
498
|
+
["--preload", "require"],
|
|
499
|
+
]),
|
|
500
|
+
flags: [
|
|
501
|
+
"--watch",
|
|
502
|
+
"--hot",
|
|
503
|
+
"--smol",
|
|
504
|
+
"--bun",
|
|
505
|
+
"--silent",
|
|
506
|
+
"--no-install",
|
|
507
|
+
"-v",
|
|
508
|
+
"--version",
|
|
509
|
+
].concat(["-h", "--help"]),
|
|
510
|
+
runsNode: false,
|
|
511
|
+
subcommands: new Set(["run"]),
|
|
512
|
+
modes: new Set(
|
|
513
|
+
["install", "i", "add", "a", "remove", "rm", "update", "outdated", "link", "unlink", "pm"]
|
|
514
|
+
.concat(["test", "build", "init", "create", "c", "upgrade", "publish", "patch", "audit"])
|
|
515
|
+
.concat(["info", "repl", "exec", "x"]),
|
|
516
|
+
),
|
|
517
|
+
inline: INLINE,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* `deno [run|serve] [options] <file> [arguments]`, https://docs.deno.com/runtime/reference/cli/run/
|
|
522
|
+
* and https://docs.deno.com/runtime/reference/cli/serve/, with the permission flags written
|
|
523
|
+
* whole; its other subcommands run no entry: https://docs.deno.com/runtime/reference/cli/,
|
|
524
|
+
* found past its global options only, so `deno -A lint` runs `lint`:
|
|
525
|
+
* https://github.com/denoland/deno/blob/main/libs/cli_parser/src/parse.rs
|
|
526
|
+
*/
|
|
527
|
+
const DENO: Runtime = {
|
|
528
|
+
preloads: new Map(),
|
|
529
|
+
flags: ["-A", "--allow-all", "-q", "--quiet", "--watch", "--no-check", "-V", "--version"].concat(
|
|
530
|
+
["-h", "--help"],
|
|
531
|
+
["read", "write", "net", "env", "run", "sys", "ffi", "import"].map((name) => `--allow-${name}`),
|
|
532
|
+
),
|
|
533
|
+
runsNode: false,
|
|
534
|
+
subcommands: new Set(["run", "serve"]),
|
|
535
|
+
modeAfter: new Set(["-q", "--quiet"]),
|
|
536
|
+
modes: new Set(
|
|
537
|
+
["task", "test", "fmt", "lint", "compile", "install", "uninstall", "eval", "repl", "bench"]
|
|
538
|
+
.concat(["check", "doc", "info", "init", "upgrade", "cache", "coverage", "types", "add"])
|
|
539
|
+
.concat(["remove", "outdated", "publish", "jupyter", "clean", "completions", "help"]),
|
|
540
|
+
),
|
|
541
|
+
};
|
|
214
542
|
|
|
215
543
|
/** Hands on the command `words` from `at`, after the assignments `assigns` accepts. */
|
|
216
544
|
function afterAssignments(
|
|
@@ -219,8 +547,12 @@ function afterAssignments(
|
|
|
219
547
|
assigns: (value: string) => boolean,
|
|
220
548
|
): Reading | undefined {
|
|
221
549
|
let end = at;
|
|
222
|
-
|
|
223
|
-
|
|
550
|
+
for (
|
|
551
|
+
let word = words[end];
|
|
552
|
+
word !== undefined && startOf(word).includes("=");
|
|
553
|
+
word = words[end]
|
|
554
|
+
) {
|
|
555
|
+
if (!assigns(startOf(word))) {
|
|
224
556
|
return undefined;
|
|
225
557
|
}
|
|
226
558
|
end += 1;
|
|
@@ -231,66 +563,140 @@ function afterAssignments(
|
|
|
231
563
|
}
|
|
232
564
|
return program === undefined
|
|
233
565
|
? { kind: "run", entry: undefined, preloads: [], runtime: undefined }
|
|
234
|
-
: { kind: "command", words: words.slice(end) };
|
|
566
|
+
: { kind: "command", words: words.slice(end), at: end };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* GNU env's options, https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html,
|
|
571
|
+
* whose `-S` and `--split-string` give a command line:
|
|
572
|
+
* https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html#g_t_002dS_002f_002d_002dsplit_002dstring-usage-in-scripts
|
|
573
|
+
*/
|
|
574
|
+
const ENV: Options = {
|
|
575
|
+
valued: new Set(["-u", "--unset", "-S", "--split-string", "-C", "--chdir"]),
|
|
576
|
+
flags: new Set([
|
|
577
|
+
...["-i", "--ignore-environment", "-", "-v", "--debug", "-0", "--null"],
|
|
578
|
+
"--list-signal-handling",
|
|
579
|
+
]),
|
|
580
|
+
attached: true,
|
|
581
|
+
optional: new Set(long("default-signal", "ignore-signal", "block-signal")),
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/** env's options that give a command line, and the one that moves it to another directory. */
|
|
585
|
+
const SPLIT: ReadonlySet<string> = new Set(["-S", "--split-string"]);
|
|
586
|
+
const CHDIR: ReadonlySet<string> = new Set(["-C", "--chdir"]);
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* The command line env runs where a `-S` or `--split-string` stands: GNU env splits its value
|
|
590
|
+
* on blanks, puts those words in place of the option and its value, before env's later words,
|
|
591
|
+
* and reads its options again from the first of them, parse_split_string of
|
|
592
|
+
* https://github.com/coreutils/coreutils/blob/master/src/env.c. {@link UNKNOWN} where the value
|
|
593
|
+
* is dynamic, writes a quote, an escape, a `$` or a `#`, which env reads otherwise, or follows
|
|
594
|
+
* a `-C`; `undefined` where none stands.
|
|
595
|
+
*/
|
|
596
|
+
function envSplit(words: readonly Word[]): string | Unknown | undefined {
|
|
597
|
+
const { options } = scanOptions(words, ENV);
|
|
598
|
+
const split = options.findIndex(({ name }) => SPLIT.has(name));
|
|
599
|
+
const value = options[split]?.value;
|
|
600
|
+
if (value === undefined) {
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
const text = words[value.at]?.value?.slice(value.prefix.length);
|
|
604
|
+
if (
|
|
605
|
+
text === undefined ||
|
|
606
|
+
/['"\\$#]/u.test(text) ||
|
|
607
|
+
options.slice(0, split).some(({ name }) => CHDIR.has(name))
|
|
608
|
+
) {
|
|
609
|
+
return UNKNOWN;
|
|
610
|
+
}
|
|
611
|
+
const spliced = text.split(/\s+/u).filter((one) => one !== "");
|
|
612
|
+
const later = words.slice(value.at + 1).map((word) => word.text);
|
|
613
|
+
return ["env", ...spliced.map((one) => `'${one}'`), ...later].join(" ");
|
|
235
614
|
}
|
|
236
615
|
|
|
237
616
|
/**
|
|
238
|
-
* `env [option]... [name=value]... [command [argument]...]
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
617
|
+
* `env [option]... [name=value]... [command [argument]...]`: the options that change only the
|
|
618
|
+
* environment, `-i`, `-u` and `-v` in each spelling, and `-S`, read by {@link envSplit}. GNU env
|
|
619
|
+
* takes any word holding an `=` after its first character as an assignment, and fails before
|
|
620
|
+
* running anything on a `-u` name that is empty or holds an `=`. A dynamic word whose written
|
|
621
|
+
* start is an assignment's is one, as env reads the text it expands to.
|
|
243
622
|
*/
|
|
244
623
|
const env: Reader = (words) => {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
624
|
+
const split = envSplit(words);
|
|
625
|
+
if (split !== undefined) {
|
|
626
|
+
return typeof split === "string" ? { kind: "line", text: split } : split;
|
|
627
|
+
}
|
|
628
|
+
const read = optionsOf(words, ENV);
|
|
629
|
+
if (read === undefined || isUnknown(read) || [...CHDIR].some((name) => read.named.has(name))) {
|
|
630
|
+
return read === undefined ? undefined : UNKNOWN;
|
|
631
|
+
}
|
|
632
|
+
const unset = [...(read.values.get("-u") ?? []), ...(read.values.get("--unset") ?? [])];
|
|
633
|
+
return unset.some((name) => name === "" || name.includes("="))
|
|
252
634
|
? undefined
|
|
253
635
|
: afterAssignments(words, read.at, (value) => !value.startsWith("="));
|
|
254
636
|
};
|
|
255
637
|
|
|
638
|
+
/** The command line env runs where a `-S` stands, as {@link envSplit} reads it. */
|
|
639
|
+
function envLines(words: readonly Word[]): Lines {
|
|
640
|
+
const split = envSplit(words);
|
|
641
|
+
return typeof split === "string" ? { ...NO_LINES, texts: [split] } : NO_LINES;
|
|
642
|
+
}
|
|
643
|
+
|
|
256
644
|
/** `cross-env [name=value]... command [argument]...`, https://github.com/kentcdodds/cross-env#usage */
|
|
257
645
|
const crossEnv: Reader = (words) => {
|
|
258
646
|
const reading = afterAssignments(words, 0, (value) => /^\w+=/u.test(value));
|
|
259
647
|
return reading?.kind === "command" ? reading : undefined;
|
|
260
648
|
};
|
|
261
649
|
|
|
650
|
+
/** The command a package runner's operand names: a package specifier's, whose version picks no other command. */
|
|
651
|
+
function commandName(operand: string, packaged: boolean): string {
|
|
652
|
+
return packaged ? operand.replace(/(?<=.)@[^/@]*$/u, "") : operand;
|
|
653
|
+
}
|
|
654
|
+
|
|
262
655
|
/** The command a package runner hands on: its first word a package's or a command's name. */
|
|
263
656
|
function runnerCommand(words: readonly Word[], packaged: boolean): Reading | undefined {
|
|
264
657
|
const [first, ...rest] = words;
|
|
265
658
|
if (first?.value === undefined || first.value.startsWith("-")) {
|
|
266
659
|
return undefined;
|
|
267
660
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
661
|
+
const name = commandName(first.value, packaged);
|
|
662
|
+
return { kind: "command", words: [{ value: name, text: first.text }, ...rest], at: 0 };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* npx's and npm exec's options, https://docs.npmjs.com/cli/v11/commands/npx and
|
|
667
|
+
* https://docs.npmjs.com/cli/v11/commands/npm-exec: `--package` (and npx's `-p`), `-c` and
|
|
668
|
+
* `--call`, which give a command line, `--yes` and `--no`.
|
|
669
|
+
*/
|
|
670
|
+
function packageExecOptions(shortPackage: boolean): Options {
|
|
671
|
+
return {
|
|
672
|
+
valued: new Set(["--package", "-c", "--call", ...(shortPackage ? ["-p"] : [])]),
|
|
673
|
+
flags: new Set(["-y", "--yes", "--no"]),
|
|
674
|
+
};
|
|
271
675
|
}
|
|
272
676
|
|
|
677
|
+
const NPX = packageExecOptions(true);
|
|
678
|
+
const NPM_EXEC = packageExecOptions(false);
|
|
679
|
+
const CALL: ReadonlySet<string> = new Set(["-c", "--call"]);
|
|
680
|
+
|
|
273
681
|
/**
|
|
274
|
-
* npx and npm exec,
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* runs unless a `--package` names one. npm exec reads options after it too, unless a `--` ends them.
|
|
682
|
+
* npx and npm exec, by `options`, before the first positional word, which is the package whose
|
|
683
|
+
* command runs unless a `--package` names one. npm exec reads options after it too, unless a
|
|
684
|
+
* `--` ends them.
|
|
278
685
|
*/
|
|
279
|
-
function packageExec(
|
|
280
|
-
const valued = new Set(["--package", "-c", "--call", ...(shortPackage ? ["-p"] : [])]);
|
|
686
|
+
function packageExec(options: Options, laterOptions: boolean): Reader {
|
|
281
687
|
return (words) => {
|
|
282
|
-
const read = optionsOf(words,
|
|
283
|
-
if (read === undefined) {
|
|
284
|
-
return
|
|
688
|
+
const read = optionsOf(words, options);
|
|
689
|
+
if (read === undefined || isUnknown(read)) {
|
|
690
|
+
return read;
|
|
285
691
|
}
|
|
286
692
|
const operands = words.slice(read.at);
|
|
287
|
-
if (
|
|
288
|
-
laterOptions &&
|
|
289
|
-
!read.ended &&
|
|
290
|
-
operands.some((word) => word.value?.startsWith("-") !== false)
|
|
291
|
-
) {
|
|
693
|
+
if (laterOptions && !read.ended && operands.some((word) => word.value === undefined)) {
|
|
292
694
|
return undefined;
|
|
293
695
|
}
|
|
696
|
+
// npm exec reads an option after its command as its own, whose table no row places.
|
|
697
|
+
if (laterOptions && !read.ended && operands.some((word) => word.value?.startsWith("-"))) {
|
|
698
|
+
return UNKNOWN;
|
|
699
|
+
}
|
|
294
700
|
const call = [...(read.values.get("-c") ?? []), ...(read.values.get("--call") ?? [])];
|
|
295
701
|
if (call.length > 0) {
|
|
296
702
|
return call.length === 1 && operands.length === 0 && call[0] !== undefined
|
|
@@ -298,12 +704,12 @@ function packageExec(shortPackage: boolean, laterOptions: boolean): Reader {
|
|
|
298
704
|
: undefined;
|
|
299
705
|
}
|
|
300
706
|
const packages = [...(read.values.get("--package") ?? []), ...(read.values.get("-p") ?? [])];
|
|
301
|
-
return runnerCommand(operands, packages.length === 0);
|
|
707
|
+
return after(read.at, runnerCommand(operands, packages.length === 0));
|
|
302
708
|
};
|
|
303
709
|
}
|
|
304
710
|
|
|
305
|
-
const npx = packageExec(
|
|
306
|
-
const npmExec = packageExec(
|
|
711
|
+
const npx = packageExec(NPX, false);
|
|
712
|
+
const npmExec = packageExec(NPM_EXEC, true);
|
|
307
713
|
|
|
308
714
|
/**
|
|
309
715
|
* `npm run <script> [-- <arguments>]`, https://docs.npmjs.com/cli/v11/commands/npm-run-script:
|
|
@@ -323,12 +729,37 @@ const npmRun: Reader = (words) => {
|
|
|
323
729
|
return { kind: "run", entry: undefined, preloads: [], runtime: undefined };
|
|
324
730
|
};
|
|
325
731
|
|
|
326
|
-
/**
|
|
327
|
-
function
|
|
328
|
-
return
|
|
329
|
-
|
|
732
|
+
/** `reading`, of the words after `skipped` others, as a reading of those words and them. */
|
|
733
|
+
function after<Read extends Reading | Unknown | undefined>(skipped: number, reading: Read): Read {
|
|
734
|
+
return reading?.kind === "command" ? { ...reading, at: reading.at + skipped } : reading;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* A tool whose second word picks the reader of the rest. Where its own options `options` stand
|
|
739
|
+
* before that word, it is not read, as they may run the command in another directory, and
|
|
740
|
+
* {@link UNKNOWN} where `options` does not hold one.
|
|
741
|
+
*/
|
|
742
|
+
function subcommands(readers: ReadonlyMap<string, Reader>, options?: Options): Reader {
|
|
743
|
+
return (words) => {
|
|
744
|
+
const [first, ...rest] = words;
|
|
745
|
+
if (options !== undefined && first?.value?.startsWith("-") === true) {
|
|
746
|
+
return isUnknown(optionsOf(words, options)) ? UNKNOWN : undefined;
|
|
747
|
+
}
|
|
748
|
+
return first?.value === undefined ? undefined : after(1, readers.get(first.value)?.(rest));
|
|
749
|
+
};
|
|
330
750
|
}
|
|
331
751
|
|
|
752
|
+
/** npm's own options before its subcommand: https://docs.npmjs.com/cli/v11/using-npm/config */
|
|
753
|
+
const NPM: Options = {
|
|
754
|
+
valued: new Set(["-w", ...long("workspace", "prefix", "loglevel")]),
|
|
755
|
+
flags: new Set([
|
|
756
|
+
...["-ws", ...letters("sqdy")],
|
|
757
|
+
...long("workspaces", "no-workspaces", "include-workspace-root", "if-present", "silent"),
|
|
758
|
+
...long("quiet", "verbose", "yes", "no"),
|
|
759
|
+
...["-v", "-h", ...long("version", "help")],
|
|
760
|
+
]),
|
|
761
|
+
};
|
|
762
|
+
|
|
332
763
|
/** `npm exec`, `npm x` and `npm run` with `run-script`. */
|
|
333
764
|
const npm = subcommands(
|
|
334
765
|
new Map([
|
|
@@ -337,58 +768,125 @@ const npm = subcommands(
|
|
|
337
768
|
["run", npmRun],
|
|
338
769
|
["run-script", npmRun],
|
|
339
770
|
]),
|
|
771
|
+
NPM,
|
|
340
772
|
);
|
|
341
773
|
|
|
774
|
+
/** `yarn dlx`'s options: https://yarnpkg.com/cli/dlx */
|
|
775
|
+
const YARN_DLX: Options = {
|
|
776
|
+
valued: new Set(["-p", "--package"]),
|
|
777
|
+
flags: new Set(["-q", "--quiet"]),
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
/** `bunx`'s options: https://bun.com/docs/pm/bunx */
|
|
781
|
+
const BUNX: Options = { valued: new Set(["-p", "--package"]), flags: new Set() };
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* pnpm's own options, which it reads before or after its subcommand, `exec`'s `-c` among them:
|
|
785
|
+
* https://pnpm.io/pnpm-cli#options, https://pnpm.io/filtering, https://pnpm.io/cli/recursive,
|
|
786
|
+
* https://pnpm.io/cli/exec
|
|
787
|
+
*/
|
|
788
|
+
const PNPM: Options = {
|
|
789
|
+
valued: new Set([
|
|
790
|
+
...["-C", "-F", ...long("dir", "filter", "filter-prod", "test-pattern", "resume-from")],
|
|
791
|
+
...long("changed-files-ignore-pattern", "workspace-concurrency", "reporter", "loglevel"),
|
|
792
|
+
]),
|
|
793
|
+
flags: new Set([
|
|
794
|
+
...letters("wrscvh"),
|
|
795
|
+
...long("version", "help"),
|
|
796
|
+
...long("workspace-root", "recursive", "parallel", "stream", "aggregate-output", "bail"),
|
|
797
|
+
...long("no-bail", "sort", "no-sort", "reverse", "report-summary", "fail-if-no-match"),
|
|
798
|
+
...long("silent", "color", "no-color", "shell-mode"),
|
|
799
|
+
]),
|
|
800
|
+
attached: true,
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
/** 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 */
|
|
804
|
+
const PNPM_LINES: ReadonlySet<string> = new Set(["-c", "--shell-mode"]);
|
|
805
|
+
|
|
806
|
+
/** pnpm's options after `dlx`, its own and `--package`. */
|
|
807
|
+
const PNPM_DLX_OPTIONS: Options = { ...PNPM, valued: new Set([...PNPM.valued, "--package"]) };
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* `pnpm exec <command>`, https://pnpm.io/cli/exec, which hands on every later word, read where
|
|
811
|
+
* no option of pnpm's stands before it, which may run it in another directory or a shell.
|
|
812
|
+
*/
|
|
813
|
+
const pnpmExec: Reader = (words) => {
|
|
814
|
+
const read = optionsOf(words, PNPM);
|
|
815
|
+
if (read === undefined || isUnknown(read)) {
|
|
816
|
+
return read;
|
|
817
|
+
}
|
|
818
|
+
return read.at > 0 ? undefined : runnerCommand(words, false);
|
|
819
|
+
};
|
|
820
|
+
|
|
342
821
|
/**
|
|
343
|
-
* `pnpm
|
|
344
|
-
*
|
|
822
|
+
* `pnpm dlx [--package <name>]... <command>`, https://pnpm.io/cli/dlx, and `pnpx`, its alias:
|
|
823
|
+
* https://pnpm.io/cli/dlx#pnpx. Another option of pnpm's stands only unread.
|
|
345
824
|
*/
|
|
825
|
+
const pnpmDlx: Reader = (words) => {
|
|
826
|
+
const read = optionsOf(words, PNPM_DLX_OPTIONS);
|
|
827
|
+
if (read === undefined || isUnknown(read)) {
|
|
828
|
+
return read;
|
|
829
|
+
}
|
|
830
|
+
return [...read.named].some((name) => name !== "--package")
|
|
831
|
+
? undefined
|
|
832
|
+
: after(read.at, runnerCommand(words.slice(read.at), !read.values.has("--package")));
|
|
833
|
+
};
|
|
834
|
+
|
|
346
835
|
const pnpm = subcommands(
|
|
347
836
|
new Map<string, Reader>([
|
|
348
|
-
["exec",
|
|
349
|
-
[
|
|
350
|
-
"dlx",
|
|
351
|
-
(words) => {
|
|
352
|
-
const read = optionsOf(words, { valued: new Set(["--package"]), flags: new Set() });
|
|
353
|
-
return read === undefined
|
|
354
|
-
? undefined
|
|
355
|
-
: runnerCommand(words.slice(read.at), !read.values.has("--package"));
|
|
356
|
-
},
|
|
357
|
-
],
|
|
837
|
+
["exec", pnpmExec],
|
|
838
|
+
["dlx", pnpmDlx],
|
|
358
839
|
]),
|
|
840
|
+
PNPM,
|
|
359
841
|
);
|
|
360
842
|
|
|
843
|
+
/** yarn's own options before its subcommand: https://yarnpkg.com/cli */
|
|
844
|
+
const YARN: Options = {
|
|
845
|
+
valued: new Set(["--cwd"]),
|
|
846
|
+
flags: new Set(["-v", "-h", ...long("version", "help")]),
|
|
847
|
+
};
|
|
848
|
+
|
|
361
849
|
/**
|
|
362
850
|
* `yarn node`, https://yarnpkg.com/cli/node, and `yarn dlx [-p <name>]... <command>`,
|
|
363
851
|
* https://yarnpkg.com/cli/dlx, whose command runs in the current directory.
|
|
364
852
|
*/
|
|
365
853
|
const yarn = subcommands(
|
|
366
854
|
new Map<string, Reader>([
|
|
367
|
-
[
|
|
855
|
+
[
|
|
856
|
+
"node",
|
|
857
|
+
(words) => ({ kind: "command", words: [{ value: "node", text: "node" }, ...words], at: -1 }),
|
|
858
|
+
],
|
|
368
859
|
[
|
|
369
860
|
"dlx",
|
|
370
861
|
(words) => {
|
|
371
|
-
const read = optionsOf(words,
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
862
|
+
const read = optionsOf(words, YARN_DLX);
|
|
863
|
+
return read === undefined || isUnknown(read)
|
|
864
|
+
? read
|
|
865
|
+
: after(
|
|
866
|
+
read.at,
|
|
867
|
+
runnerCommand(
|
|
868
|
+
words.slice(read.at),
|
|
869
|
+
!read.values.has("-p") && !read.values.has("--package"),
|
|
870
|
+
),
|
|
380
871
|
);
|
|
381
872
|
},
|
|
382
873
|
],
|
|
383
874
|
]),
|
|
875
|
+
YARN,
|
|
384
876
|
);
|
|
385
877
|
|
|
386
878
|
/** `bunx [-p <name>] <command>[@version] [arguments]`, https://bun.com/docs/pm/bunx */
|
|
387
879
|
const bunx: Reader = (words) => {
|
|
388
|
-
const read = optionsOf(words,
|
|
389
|
-
return read === undefined
|
|
880
|
+
const read = optionsOf(words, BUNX);
|
|
881
|
+
return read === undefined || isUnknown(read)
|
|
882
|
+
? read
|
|
883
|
+
: after(read.at, runnerCommand(words.slice(read.at), true));
|
|
390
884
|
};
|
|
391
885
|
|
|
886
|
+
/** `bun x`, which is `bunx`, and else bun's own command line. */
|
|
887
|
+
const bun: Reader = (words) =>
|
|
888
|
+
words[0]?.value === "x" ? after(1, bunx(words.slice(1))) : runtime(BUN)(words);
|
|
889
|
+
|
|
392
890
|
/**
|
|
393
891
|
* The options a tool documents for naming its configuration file, each taking the file as its
|
|
394
892
|
* value, the next word or, for a long one, the text after `=`.
|
|
@@ -399,80 +897,1352 @@ interface ConfigurationOptions {
|
|
|
399
897
|
readonly beforeOperand?: ReadonlySet<string>;
|
|
400
898
|
}
|
|
401
899
|
|
|
402
|
-
/**
|
|
403
|
-
interface
|
|
404
|
-
|
|
405
|
-
readonly
|
|
900
|
+
/** Where the words after a tool's program word write the command lines it hands to a shell. */
|
|
901
|
+
interface Lines {
|
|
902
|
+
/** The places of the words that may each be one, each with the option text it starts with. */
|
|
903
|
+
readonly at: ReadonlyMap<number, string>;
|
|
904
|
+
/** Whether it reads command lines from its standard input. */
|
|
905
|
+
readonly input: boolean;
|
|
906
|
+
/** Whether it runs the first word a tool before it appends, as `xargs sh -c` and `xargs bash` do. */
|
|
907
|
+
readonly appended?: boolean;
|
|
908
|
+
/**
|
|
909
|
+
* Whether it may run a file no word writes: a script none writes, as `bash -- "$1"` runs, or
|
|
910
|
+
* one after an option its table does not list.
|
|
911
|
+
*/
|
|
912
|
+
readonly unwritten?: boolean;
|
|
913
|
+
/** The places, each from one word up to before another, whose words joined by blanks are one command line. */
|
|
914
|
+
readonly joined?: readonly (readonly [number, number])[];
|
|
915
|
+
/** The command lines it runs that no word writes whole, as env runs its `-S` value's words before its later ones. */
|
|
916
|
+
readonly texts?: readonly string[];
|
|
917
|
+
/** The texts it replaces, in each word at {@link at}, with a part only the run knows. */
|
|
918
|
+
readonly replaced?: RegExp;
|
|
406
919
|
}
|
|
407
920
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
921
|
+
const NO_LINES: Lines = { at: new Map(), input: false };
|
|
922
|
+
|
|
923
|
+
/** The places `at`, each a whole word that is a command line. */
|
|
924
|
+
function wholeWords(at: readonly number[]): ReadonlyMap<number, string> {
|
|
925
|
+
return new Map(at.map((one) => [one, ""]));
|
|
411
926
|
}
|
|
412
927
|
|
|
413
|
-
/**
|
|
414
|
-
|
|
415
|
-
|
|
928
|
+
/** How the words after the program word of a tool that hands its commands the paths it finds read. */
|
|
929
|
+
interface Appending {
|
|
930
|
+
/** Where each command it runs starts, `undefined` where it has an option or a construct no row reads. */
|
|
931
|
+
readonly starts: readonly number[] | undefined;
|
|
932
|
+
/** The texts it replaces with each path, in each word from `from` on. */
|
|
933
|
+
readonly replaced: RegExp | undefined;
|
|
934
|
+
readonly from: number;
|
|
935
|
+
/** Where its command's words end and the paths it is handed begin, as at `parallel`'s `:::`. */
|
|
936
|
+
readonly end?: number;
|
|
416
937
|
}
|
|
417
938
|
|
|
418
|
-
/**
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
[
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
configurations: {
|
|
432
|
-
valued: new Set(["-c"]),
|
|
433
|
-
beforeOperand: new Set(["--package", "-p", "--call"]),
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
["npm", reading(npm)],
|
|
438
|
-
["pnpm", reading(pnpm)],
|
|
439
|
-
["yarn", reading(yarn)],
|
|
440
|
-
["bunx", reading(bunx)],
|
|
441
|
-
// https://eslint.org/docs/latest/use/command-line-interface
|
|
442
|
-
["eslint", configuredBy("-c", "--config")],
|
|
443
|
-
// https://prettier.io/docs/cli, whose `-c` is `--check`.
|
|
444
|
-
["prettier", configuredBy("--config")],
|
|
445
|
-
// https://stylelint.io/user-guide/cli
|
|
446
|
-
["stylelint", configuredBy("-c", "--config")],
|
|
447
|
-
// https://jestjs.io/docs/cli
|
|
448
|
-
["jest", configuredBy("-c", "--config")],
|
|
449
|
-
// https://vitest.dev/guide/cli
|
|
450
|
-
["vitest", configuredBy("-c", "--config")],
|
|
451
|
-
// https://vite.dev/guide/cli
|
|
452
|
-
["vite", configuredBy("-c", "--config")],
|
|
453
|
-
// https://webpack.js.org/api/cli/
|
|
454
|
-
["webpack", configuredBy("-c", "--config")],
|
|
455
|
-
// https://mochajs.org/#command-line-usage, whose `-c` is `--color`.
|
|
456
|
-
["mocha", configuredBy("--config")],
|
|
457
|
-
]);
|
|
939
|
+
/** How a tool runs a command written after it. */
|
|
940
|
+
interface Launch {
|
|
941
|
+
/** The subcommands it runs one after, where it runs one only after a subcommand. */
|
|
942
|
+
readonly after?: ReadonlySet<string>;
|
|
943
|
+
/** Its own options, which may stand before its subcommand. */
|
|
944
|
+
readonly options?: Options;
|
|
945
|
+
/** For a tool that hands each command it runs the paths it finds, how its words read. */
|
|
946
|
+
readonly appends?: (words: readonly Word[]) => Appending;
|
|
947
|
+
/** For a tool that hands command lines to a shell, where its words write them. */
|
|
948
|
+
readonly lines?: (words: readonly Word[]) => Lines;
|
|
949
|
+
/** Whether it runs a command only as a line {@link lines} places, as a shell does. */
|
|
950
|
+
readonly line?: boolean;
|
|
951
|
+
}
|
|
458
952
|
|
|
459
|
-
/**
|
|
460
|
-
function
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
: /^(?:.*\/)?node_modules\/\.bin\/([^/]+)$|^([^/]+)$/u.exec(program.value);
|
|
465
|
-
const tool = name?.[1] ?? name?.[2];
|
|
466
|
-
return tool === undefined ? undefined : TOOLS.get(tool);
|
|
953
|
+
/** Whether a word's written text, the text beside its parts, writes a path: text other than `/`, `\`, `.` and `..`. */
|
|
954
|
+
function writesPath(written: string | undefined): boolean {
|
|
955
|
+
return (written ?? "")
|
|
956
|
+
.split(/[/\\]/u)
|
|
957
|
+
.some((segment) => !["", ".", ".."].includes(segment.replaceAll(PART, "")));
|
|
467
958
|
}
|
|
468
959
|
|
|
469
960
|
/**
|
|
470
|
-
*
|
|
471
|
-
*
|
|
961
|
+
* A shell's options: POSIX sh's one-letter ones and the `set` ones bash takes, which may share a
|
|
962
|
+
* word and may start with `+`, https://pubs.opengroup.org/onlinepubs/9799919799/utilities/sh.html,
|
|
963
|
+
* and bash's `-o`, `-O`, `--rcfile` and `--init-file`, which take the next word, and its long
|
|
964
|
+
* flags: https://www.gnu.org/software/bash/manual/html_node/Invoking-Bash.html
|
|
472
965
|
*/
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
966
|
+
const SHELL_OPTIONS: Options = {
|
|
967
|
+
valued: new Set(["-o", "-O", "--rcfile", "--init-file"]),
|
|
968
|
+
flags: new Set([
|
|
969
|
+
...letters("abCefhimnuvxlsrBEHPTckptD"),
|
|
970
|
+
...long("login", "norc", "noprofile", "posix", "restricted", "verbose", "noediting"),
|
|
971
|
+
...long("debugger", "dump-po-strings", "dump-strings", "help", "version"),
|
|
972
|
+
]),
|
|
973
|
+
attached: true,
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
/** A shell's one-letter options that give its operand as a command line, and the one that has it read its standard input. */
|
|
977
|
+
const SHELL_LINES: ReadonlySet<string> = new Set(["-c"]);
|
|
978
|
+
const SHELL_INPUT = "-s";
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* A shell's command lines: with an option holding `c`, or a dynamic word before its first
|
|
982
|
+
* operand whose written text is only letters, `-` and `+`, each such word and that operand; its
|
|
983
|
+
* standard input where neither stands and no operand names a script, or an option holding `s`
|
|
984
|
+
* or such a word may say so; else its first operand is its script, appended where none stands,
|
|
985
|
+
* its options read by {@link SHELL_OPTIONS}. This is the shell's own scan, not
|
|
986
|
+
* {@link scanOptions}: `-c` is a flag whose operand is the command line, never an attached value.
|
|
987
|
+
*/
|
|
988
|
+
function shellLines(words: readonly Word[]): Lines {
|
|
989
|
+
const dynamic: number[] = [];
|
|
990
|
+
let command = false;
|
|
991
|
+
let input = false;
|
|
992
|
+
let unread = false;
|
|
993
|
+
let operand = words.length;
|
|
994
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
995
|
+
const value = words[at]?.value;
|
|
996
|
+
const written = words[at]?.written?.replaceAll(PART, "");
|
|
997
|
+
if (value === undefined && (written === undefined || /^[A-Za-z+-]*$/u.test(written))) {
|
|
998
|
+
dynamic.push(at);
|
|
999
|
+
} else if (value === undefined) {
|
|
1000
|
+
operand = at;
|
|
1001
|
+
break;
|
|
1002
|
+
} else if (value === "--" || value === "-" || !/^[-+]/u.test(value)) {
|
|
1003
|
+
operand = value === "--" || value === "-" ? at + 1 : at;
|
|
1004
|
+
break;
|
|
1005
|
+
} else {
|
|
1006
|
+
const named = value.startsWith("--")
|
|
1007
|
+
? [value]
|
|
1008
|
+
: Array.from(value.slice(1), (one) => `-${one}`);
|
|
1009
|
+
command ||= value.startsWith("-") && named.some((one) => SHELL_LINES.has(one));
|
|
1010
|
+
input ||= value.startsWith("-") && named.includes(SHELL_INPUT);
|
|
1011
|
+
unread ||= named.some(
|
|
1012
|
+
(one) => !SHELL_OPTIONS.flags.has(one) && !SHELL_OPTIONS.valued.has(one),
|
|
1013
|
+
);
|
|
1014
|
+
at += SHELL_OPTIONS.valued.has(named.at(-1) ?? "") ? 1 : 0;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const lines = command || dynamic.length > 0;
|
|
1018
|
+
const script = command || input ? undefined : words[operand];
|
|
1019
|
+
return {
|
|
1020
|
+
at: wholeWords(lines ? [...dynamic, ...(operand < words.length ? [operand] : [])] : []),
|
|
1021
|
+
input: !command && (input || dynamic.length > 0 || operand >= words.length),
|
|
1022
|
+
appended: !input && operand >= words.length,
|
|
1023
|
+
unwritten:
|
|
1024
|
+
unread || (script !== undefined && script.value === undefined && !writesPath(script.written)),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** Every word after the program word, each a command line. */
|
|
1029
|
+
function everyWord(words: readonly Word[]): Lines {
|
|
1030
|
+
return { at: wholeWords(words.map((_, at) => at)), input: false };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* The values of the options `lines` among a tool's options `options`, each a command line, as
|
|
1035
|
+
* {@link scanOptions} reads them before its first operand or, where `later`, before a `--`,
|
|
1036
|
+
* each with the text its word writes before it. Where an option `options` does not hold
|
|
1037
|
+
* stands, every later word may be one.
|
|
1038
|
+
*/
|
|
1039
|
+
function optionLines(
|
|
1040
|
+
options: Options,
|
|
1041
|
+
lines: ReadonlySet<string>,
|
|
1042
|
+
later = false,
|
|
1043
|
+
): (words: readonly Word[]) => Lines {
|
|
1044
|
+
return (words) => {
|
|
1045
|
+
const at = new Map<number, string>();
|
|
1046
|
+
for (let from = 0; from < words.length;) {
|
|
1047
|
+
const scan = scanOptions(words, options, from);
|
|
1048
|
+
for (const { name, value } of scan.options) {
|
|
1049
|
+
if (value !== undefined && value.at < words.length && lines.has(name)) {
|
|
1050
|
+
at.set(value.at, value.prefix);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (scan.stop === "unread") {
|
|
1054
|
+
for (let one = scan.at; one < words.length; one += 1) {
|
|
1055
|
+
at.set(one, "");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
if (scan.stop !== "operand" || !later) {
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
from = scan.at + 1;
|
|
1062
|
+
}
|
|
1063
|
+
return { at, input: false };
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* The lines of a tool whose subcommand `names`, after its own options `options`, reads its
|
|
1069
|
+
* words, and those options, as `read` does; where one of those options is unread, any word may be one.
|
|
1070
|
+
*/
|
|
1071
|
+
function subcommandLines(
|
|
1072
|
+
names: ReadonlySet<string>,
|
|
1073
|
+
read: (words: readonly Word[], before: readonly Word[]) => Lines,
|
|
1074
|
+
options?: Options,
|
|
1075
|
+
): (words: readonly Word[]) => Lines {
|
|
1076
|
+
return (words) => {
|
|
1077
|
+
const sub = options === undefined ? 0 : operandAt(words, options);
|
|
1078
|
+
if (sub === undefined) {
|
|
1079
|
+
return everyWord(words);
|
|
1080
|
+
}
|
|
1081
|
+
const name = words[sub]?.value;
|
|
1082
|
+
if (name === undefined || !names.has(name)) {
|
|
1083
|
+
return NO_LINES;
|
|
1084
|
+
}
|
|
1085
|
+
const found = read(words.slice(sub + 1), words.slice(0, sub)).at;
|
|
1086
|
+
return { at: new Map([...found].map(([at, prefix]) => [at + sub + 1, prefix])), input: false };
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** The lines of `pnpm dlx` or `pnpx`, its words, after pnpm's options `before`, as {@link pnpmLines} reads them. */
|
|
1091
|
+
function dlxLines(words: readonly Word[], before: readonly Word[] = []): Lines {
|
|
1092
|
+
const scans = [scanOptions(before, PNPM), scanOptions(words, PNPM_DLX_OPTIONS)];
|
|
1093
|
+
return scans.some(
|
|
1094
|
+
(scan) => scan.stop === "unread" || scan.options.some(({ name }) => PNPM_LINES.has(name)),
|
|
1095
|
+
)
|
|
1096
|
+
? everyWord(words)
|
|
1097
|
+
: NO_LINES;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* The lines of `pnpm exec` and `pnpm dlx`: every operand where one of {@link PNPM_LINES}
|
|
1102
|
+
* stands among pnpm's options before or after the subcommand, or where one of them is unread.
|
|
1103
|
+
*/
|
|
1104
|
+
const pnpmLines = subcommandLines(new Set(["exec", "dlx"]), dlxLines, PNPM);
|
|
1105
|
+
|
|
1106
|
+
/** Each of `texts` as a pattern of each place it stands in a word, or `undefined` where none is. */
|
|
1107
|
+
function textsPattern(texts: readonly string[]): RegExp | undefined {
|
|
1108
|
+
const written = texts.filter((text) => text !== "");
|
|
1109
|
+
return written.length === 0
|
|
1110
|
+
? undefined
|
|
1111
|
+
: new RegExp(
|
|
1112
|
+
written.map((text) => text.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("|"),
|
|
1113
|
+
"gu",
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** The words of an appending tool with an option or a construct no row reads. */
|
|
1118
|
+
const UNREAD: Appending = { starts: undefined, replaced: undefined, from: 0 };
|
|
1119
|
+
|
|
1120
|
+
/** The long options named `names`. */
|
|
1121
|
+
function long(...names: readonly string[]): readonly string[] {
|
|
1122
|
+
return names.map((name) => `--${name}`);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** The one-letter options `letters` name. */
|
|
1126
|
+
function letters(written: string): readonly string[] {
|
|
1127
|
+
return Array.from(written, (letter) => `-${letter}`);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/** https://www.gnu.org/software/findutils/manual/html_node/find_html/xargs-options.html */
|
|
1131
|
+
const XARGS: Options = {
|
|
1132
|
+
valued: new Set([
|
|
1133
|
+
...letters("adEILnPs"),
|
|
1134
|
+
...long("arg-file", "delimiter", "max-args", "max-procs", "max-chars", "process-slot-var"),
|
|
1135
|
+
]),
|
|
1136
|
+
flags: new Set([
|
|
1137
|
+
...letters("0prtxo"),
|
|
1138
|
+
...long("null", "interactive", "no-run-if-empty", "verbose", "exit", "open-tty", "show-limits"),
|
|
1139
|
+
]),
|
|
1140
|
+
attached: true,
|
|
1141
|
+
optional: new Set(["-e", "-i", "-l", ...long("eof", "replace", "max-lines")]),
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* `xargs [option]... [command [argument]...]`: its command starts after its options, and `-I`,
|
|
1146
|
+
* `-i` and `--replace` name the text it replaces with each path it reads, `{}` where `-i` or
|
|
1147
|
+
* `--replace` names none. With no command it runs `echo`.
|
|
1148
|
+
*/
|
|
1149
|
+
function xargs(words: readonly Word[]): Appending {
|
|
1150
|
+
const read = optionsOf(words, XARGS);
|
|
1151
|
+
if (read === undefined || isUnknown(read)) {
|
|
1152
|
+
return UNREAD;
|
|
1153
|
+
}
|
|
1154
|
+
const defaulted = [...(read.values.get("-i") ?? []), ...(read.values.get("--replace") ?? [])];
|
|
1155
|
+
return {
|
|
1156
|
+
starts: [read.at],
|
|
1157
|
+
replaced: textsPattern([
|
|
1158
|
+
...(read.values.get("-I") ?? []),
|
|
1159
|
+
...defaulted.map((text) => (text === "" ? "{}" : text)),
|
|
1160
|
+
]),
|
|
1161
|
+
from: read.at,
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/** `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 */
|
|
1166
|
+
const FIND_ACTIONS = new Set(["-exec", "-execdir", "-ok", "-okdir"]);
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* GNU find's options and primaries that take the next word, two for `-fprintf`:
|
|
1170
|
+
* https://www.gnu.org/software/findutils/manual/html_node/find_html/Primary-Index.html
|
|
1171
|
+
*/
|
|
1172
|
+
const FIND_VALUED = new Set([
|
|
1173
|
+
...["-D", "-name", "-iname", "-path", "-ipath", "-wholename", "-iwholename", "-regex"],
|
|
1174
|
+
...["-iregex", "-lname", "-ilname", "-type", "-xtype", "-user", "-group", "-uid", "-gid"],
|
|
1175
|
+
...["-perm", "-size", "-mtime", "-atime", "-ctime", "-mmin", "-amin", "-cmin", "-newer"],
|
|
1176
|
+
...["-anewer", "-cnewer", "-inum", "-links", "-samefile", "-used", "-fstype", "-context"],
|
|
1177
|
+
...["-maxdepth", "-mindepth", "-printf", "-fprint", "-fprint0", "-fls", "-regextype"],
|
|
1178
|
+
...["-files0-from", "-fprintf"],
|
|
1179
|
+
]);
|
|
1180
|
+
|
|
1181
|
+
/** find's `-newerXY` primaries, which take the next word too. */
|
|
1182
|
+
const FIND_NEWER = /^-newer[aBcm][aBcmt]$/u;
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* `find`'s words: its paths, up to the first that starts with `-` or is `(` or `!`, then its
|
|
1186
|
+
* expression. A dynamic word that is no primary's value, nor a path the expression follows,
|
|
1187
|
+
* may be an action, a construct no row reads.
|
|
1188
|
+
*/
|
|
1189
|
+
function find(words: readonly Word[]): Appending {
|
|
1190
|
+
const starts: number[] = [];
|
|
1191
|
+
let paths = true;
|
|
1192
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
1193
|
+
const value = words[at]?.value;
|
|
1194
|
+
paths &&= value === undefined || !(value.startsWith("-") || value === "(" || value === "!");
|
|
1195
|
+
const next = words[at + 1]?.value;
|
|
1196
|
+
if (value === undefined) {
|
|
1197
|
+
if (paths && (next === undefined ? at + 1 === words.length : /^[-(!]/u.test(next))) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
return UNREAD;
|
|
1201
|
+
}
|
|
1202
|
+
if (FIND_VALUED.has(value) || FIND_NEWER.test(value)) {
|
|
1203
|
+
at += value === "-fprintf" ? 2 : 1;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (FIND_ACTIONS.has(value)) {
|
|
1207
|
+
starts.push(at + 1);
|
|
1208
|
+
at = words.findIndex(
|
|
1209
|
+
(word, end) =>
|
|
1210
|
+
end > at &&
|
|
1211
|
+
(word.value === ";" || (word.value === "+" && words[end - 1]?.value === "{}")),
|
|
1212
|
+
);
|
|
1213
|
+
at = at < 0 ? words.length : at;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return { starts, replaced: /\{\}/gu, from: starts[0] ?? words.length };
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/** GNU parallel's options naming a text other than a `{…}` that it replaces with a part of each path. */
|
|
1220
|
+
const PARALLEL_REPLACING = [
|
|
1221
|
+
"-I",
|
|
1222
|
+
...long("er", "extensionreplace", "bnr", "basenamereplace", "dnr", "dirnamereplace"),
|
|
1223
|
+
...long("bner", "basenameextensionreplace", "seqreplace", "slotreplace"),
|
|
1224
|
+
];
|
|
1225
|
+
|
|
1226
|
+
/** https://www.gnu.org/software/parallel/parallel.html */
|
|
1227
|
+
const PARALLEL: Options = {
|
|
1228
|
+
valued: new Set([
|
|
1229
|
+
...PARALLEL_REPLACING,
|
|
1230
|
+
...letters("jNnLaCdSE"),
|
|
1231
|
+
...long("jobs", "max-args", "max-lines", "arg-file", "colsep", "delimiter", "sshlogin"),
|
|
1232
|
+
...long("joblog", "results", "delay", "timeout", "halt", "tmpdir", "workdir", "eof"),
|
|
1233
|
+
]),
|
|
1234
|
+
flags: new Set([
|
|
1235
|
+
...letters("kvq0uXmr"),
|
|
1236
|
+
...long("keep-order", "verbose", "bar", "progress", "eta", "tag", "dry-run", "null"),
|
|
1237
|
+
...long("quote", "ungroup", "group", "line-buffer", "lb", "xargs", "no-notice"),
|
|
1238
|
+
...long("will-cite", "shuf", "plus", "no-run-if-empty", "pipe"),
|
|
1239
|
+
]),
|
|
1240
|
+
attached: true,
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* `parallel [option]... [command [argument]...] [::: argument... | :::: file...]`: its command
|
|
1245
|
+
* starts after its options and ends at the first `:::` or `::::`, and every `{…}` in it, each
|
|
1246
|
+
* one of the replacement strings it documents, and each text its replacing options name
|
|
1247
|
+
* stand for a part of each path it is handed. A `{=…=}`, the path a perl expression computes,
|
|
1248
|
+
* is a construct no row reads.
|
|
1249
|
+
*/
|
|
1250
|
+
function parallel(words: readonly Word[]): Appending {
|
|
1251
|
+
const read = optionsOf(words, PARALLEL);
|
|
1252
|
+
if (read === undefined || isUnknown(read)) {
|
|
1253
|
+
return UNREAD;
|
|
1254
|
+
}
|
|
1255
|
+
const end = words.findIndex((word, at) => at >= read.at && /^::::?\+?$/u.test(word.value ?? ""));
|
|
1256
|
+
const command = words.slice(read.at, end < 0 ? undefined : end);
|
|
1257
|
+
// The word parallel is handed, its quotes removed, as `'{''=…=}'` writes `{=…=}`.
|
|
1258
|
+
if (command.some((word) => (word.value ?? word.written ?? word.text).includes("{="))) {
|
|
1259
|
+
return UNREAD;
|
|
1260
|
+
}
|
|
1261
|
+
const texts = textsPattern(PARALLEL_REPLACING.flatMap((name) => read.values.get(name) ?? []));
|
|
1262
|
+
return {
|
|
1263
|
+
starts: [read.at],
|
|
1264
|
+
replaced: new RegExp(
|
|
1265
|
+
[/\{[^{}\s]*\}/u.source, ...(texts === undefined ? [] : [texts.source])].join("|"),
|
|
1266
|
+
"gu",
|
|
1267
|
+
),
|
|
1268
|
+
from: read.at,
|
|
1269
|
+
...(end < 0 ? {} : { end }),
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* `parallel` hands a shell its command's words joined by blanks as one command line; with no
|
|
1275
|
+
* command before its `:::` each word after it, and with none at all each line it reads.
|
|
1276
|
+
*/
|
|
1277
|
+
function parallelLines(words: readonly Word[]): Lines {
|
|
1278
|
+
const { starts, end } = parallel(words);
|
|
1279
|
+
const [start] = starts ?? [];
|
|
1280
|
+
if (start === undefined) {
|
|
1281
|
+
return everyWord(words);
|
|
1282
|
+
}
|
|
1283
|
+
if (start === end) {
|
|
1284
|
+
return { at: wholeWords(words.slice(start + 1).map((_, at) => start + 1 + at)), input: false };
|
|
1285
|
+
}
|
|
1286
|
+
return start === words.length
|
|
1287
|
+
? { at: new Map(), input: true }
|
|
1288
|
+
: { at: new Map(), input: false, joined: [[start, end ?? words.length]] };
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* concurrently's options, read by yargs before and after its commands, each a long one's
|
|
1293
|
+
* `=` spelling too: https://github.com/open-cli-tools/concurrently/blob/main/bin/index.ts
|
|
1294
|
+
*/
|
|
1295
|
+
const CONCURRENTLY: Options = {
|
|
1296
|
+
valued: new Set([
|
|
1297
|
+
...["-m", "-n", "-s", "-p", "-c", "-l", "-t"],
|
|
1298
|
+
...long("max-processes", "names", "success", "hide", "shell", "teardown", "kill-signal"),
|
|
1299
|
+
...long("kill-timeout", "prefix", "prefix-colors", "prefix-length", "timestamp-format"),
|
|
1300
|
+
...long("restart-tries", "restart-after", "default-input-target", "name-separator"),
|
|
1301
|
+
]),
|
|
1302
|
+
flags: new Set([
|
|
1303
|
+
...["-r", "-g", "-P", "-k", "-i", "-h", "-v", "-V"],
|
|
1304
|
+
...long("raw", "no-color", "group", "timings", "passthrough-arguments", "kill-others"),
|
|
1305
|
+
...long("kill-others-on-fail", "pad-prefix", "handle-input", "help", "version"),
|
|
1306
|
+
]),
|
|
1307
|
+
};
|
|
1308
|
+
|
|
1309
|
+
/** concurrently's `--teardown`, whose value is a command line it runs before it exits. */
|
|
1310
|
+
const TEARDOWN = "--teardown";
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* The words with a `{N}`, `{@}` or `{*}`, which concurrently replaces with the arguments after
|
|
1314
|
+
* its `--` under `-P`, a part only the run knows: a `CONCURRENTLY_PASSTHROUGH_ARGUMENTS`
|
|
1315
|
+
* variable, which yargs's `.env` reads, sets `-P` where no word writes it.
|
|
1316
|
+
*/
|
|
1317
|
+
const PLACEHOLDER = /\{(?:\d+|@|\*)\}/gu;
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* concurrently's command lines: each word that is no option nor an option's value, each
|
|
1321
|
+
* `--teardown` value, and the words after a `--` but under `-P`, where they are its commands'
|
|
1322
|
+
* arguments. From an option {@link CONCURRENTLY} does not list, every word may be one.
|
|
1323
|
+
*/
|
|
1324
|
+
function concurrentlyLines(words: readonly Word[]): Lines {
|
|
1325
|
+
const at = new Map<number, string>();
|
|
1326
|
+
let passthrough = false;
|
|
1327
|
+
for (let one = 0; one < words.length; one += 1) {
|
|
1328
|
+
const value = words[one]?.value;
|
|
1329
|
+
if (value === "--") {
|
|
1330
|
+
for (let later = one + 1; !passthrough && later < words.length; later += 1) {
|
|
1331
|
+
at.set(later, "");
|
|
1332
|
+
}
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
if (value === undefined || value === "-" || !value.startsWith("-")) {
|
|
1336
|
+
at.set(one, "");
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const equals = value.startsWith("--") ? value.indexOf("=") : -1;
|
|
1340
|
+
const name = equals < 0 ? value : value.slice(0, equals);
|
|
1341
|
+
if (equals < 0 && CONCURRENTLY.flags.has(name)) {
|
|
1342
|
+
passthrough ||= name === "-P" || name === "--passthrough-arguments";
|
|
1343
|
+
} else if (CONCURRENTLY.valued.has(name)) {
|
|
1344
|
+
const valueAt = equals < 0 ? (one += 1) : one;
|
|
1345
|
+
if (name === TEARDOWN && valueAt < words.length) {
|
|
1346
|
+
at.set(valueAt, equals < 0 ? "" : `${TEARDOWN}=`);
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
for (let later = one; later < words.length; later += 1) {
|
|
1350
|
+
at.set(later, "");
|
|
1351
|
+
}
|
|
1352
|
+
return { at, input: false, unwritten: true, replaced: PLACEHOLDER };
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return { at, input: false, replaced: PLACEHOLDER };
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** `eval`'s operands joined by blanks, one command line: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#eval */
|
|
1359
|
+
function evalLines(words: readonly Word[]): Lines {
|
|
1360
|
+
const from = words[0]?.value === "--" ? 1 : 0;
|
|
1361
|
+
return from >= words.length ? NO_LINES : { ...NO_LINES, joined: [[from, words.length]] };
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* The script `.` and `source` run in the shell itself, their first operand: one written with
|
|
1366
|
+
* no path runs a file no word writes, as `bash "$F"` does, and so does an option, which POSIX
|
|
1367
|
+
* gives them none of: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#dot
|
|
1368
|
+
*/
|
|
1369
|
+
function sourceLines(words: readonly Word[]): Lines {
|
|
1370
|
+
const script = words[words[0]?.value === "--" ? 1 : 0];
|
|
1371
|
+
return {
|
|
1372
|
+
...NO_LINES,
|
|
1373
|
+
unwritten:
|
|
1374
|
+
script !== undefined &&
|
|
1375
|
+
(script.value === undefined
|
|
1376
|
+
? !writesPath(script.written)
|
|
1377
|
+
: script.value.startsWith("-") && script.value !== "--"),
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/** One tool's row: the reader of its command, and the options it names its configuration file by. */
|
|
1382
|
+
interface Row {
|
|
1383
|
+
readonly read?: Reader;
|
|
1384
|
+
readonly configurations?: ConfigurationOptions;
|
|
1385
|
+
/** For a runtime, its command line. */
|
|
1386
|
+
readonly runtime?: Runtime;
|
|
1387
|
+
/** For a tool that runs a command written after it, how. */
|
|
1388
|
+
readonly launches?: Launch;
|
|
1389
|
+
/** Its options as its documentation gives them, which its command lines are placed by where its launch places none. */
|
|
1390
|
+
readonly grammar?: Grammar;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* The row of a launcher whose reader is `read`, which runs a command after `after` where
|
|
1395
|
+
* given, read after its own options `options`.
|
|
1396
|
+
*/
|
|
1397
|
+
function launcher(
|
|
1398
|
+
read: Reader | undefined,
|
|
1399
|
+
after: readonly string[] = [],
|
|
1400
|
+
lines?: Launch["lines"],
|
|
1401
|
+
options?: Options,
|
|
1402
|
+
): Row {
|
|
1403
|
+
return {
|
|
1404
|
+
...(read === undefined ? {} : { read }),
|
|
1405
|
+
launches: {
|
|
1406
|
+
...(after.length === 0 ? {} : { after: new Set(after) }),
|
|
1407
|
+
...(lines === undefined ? {} : { lines }),
|
|
1408
|
+
...(options === undefined ? {} : { options }),
|
|
1409
|
+
},
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/** The row of a runtime. */
|
|
1414
|
+
function runs(line: Runtime): Row {
|
|
1415
|
+
return { read: runtime(line), runtime: line };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** The row of a tool that names its configuration file by `options` anywhere before a `--`. */
|
|
1419
|
+
function configuredBy(...options: readonly string[]): Row {
|
|
1420
|
+
return { configurations: { valued: new Set(options) } };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/** The row of a shell, whose `-c` runs its operand as a command line. */
|
|
1424
|
+
const SHELL: Row = { launches: { line: true, lines: shellLines } };
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* The reader of a tool that runs the command after its options `options`, the first operand,
|
|
1428
|
+
* or where `ended` the first word after a `--`, before which only its options stand.
|
|
1429
|
+
*/
|
|
1430
|
+
function afterOptions(options: Options, ended = false): Reader {
|
|
1431
|
+
return (words) => {
|
|
1432
|
+
const end = words.findIndex((word) => word.value === "--");
|
|
1433
|
+
const read = optionsOf(ended && end >= 0 ? words.slice(0, end) : words, options);
|
|
1434
|
+
if (read === undefined || isUnknown(read)) {
|
|
1435
|
+
return read;
|
|
1436
|
+
}
|
|
1437
|
+
if (ended && end >= 0 && read.at !== end) {
|
|
1438
|
+
return undefined;
|
|
1439
|
+
}
|
|
1440
|
+
const at = ended && end >= 0 ? end + 1 : read.at;
|
|
1441
|
+
const program = words[at];
|
|
1442
|
+
return program?.value === undefined || program.value.startsWith("-")
|
|
1443
|
+
? undefined
|
|
1444
|
+
: { kind: "command", words: words.slice(at), at };
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* c8's options, `c8 [opts] [script] [opts]`, https://github.com/bcoe/c8#cli-options, read by
|
|
1450
|
+
* yargs, which clusters no valued option's value: https://github.com/bcoe/c8/blob/main/lib/parse-args.js
|
|
1451
|
+
*/
|
|
1452
|
+
const C8: Options = {
|
|
1453
|
+
valued: new Set([
|
|
1454
|
+
...["-c", "-r", "-o", "-n", "-x", "-e"],
|
|
1455
|
+
...long("config", "reporter", "reports-dir", "report-dir", "src", "include", "exclude"),
|
|
1456
|
+
...long("extension", "branches", "functions", "lines", "statements", "temp-directory"),
|
|
1457
|
+
...long("resolve", "wrapper-length"),
|
|
1458
|
+
]),
|
|
1459
|
+
flags: new Set([
|
|
1460
|
+
"-a",
|
|
1461
|
+
...long("all", "exclude-node-modules", "exclude-after-remap", "skip-full", "check-coverage"),
|
|
1462
|
+
...long("per-file", "100", "clean", "omit-relative", "allowExternal", "merge-async"),
|
|
1463
|
+
...long("experimental-monocart", "no-clean", "no-exclude-node-modules", "no-omit-relative"),
|
|
1464
|
+
]),
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* nyc's options, `nyc [options] [bin-to-instrument]`, https://github.com/istanbuljs/nyc#usage,
|
|
1469
|
+
* each of `@istanbuljs/schema`'s with its alias: https://github.com/istanbuljs/schema/blob/master/index.js
|
|
1470
|
+
*/
|
|
1471
|
+
const NYC: Options = {
|
|
1472
|
+
valued: new Set([
|
|
1473
|
+
...["-t", "-x", "-n", "-e", "-i", "-r"],
|
|
1474
|
+
...long("cwd", "temp-dir", "exclude", "include", "extension", "ignore-class-methods"),
|
|
1475
|
+
...long("parser-plugins", "require", "reporter", "report-dir", "cache-dir", "nycrc-path"),
|
|
1476
|
+
...long("branches", "functions", "lines", "statements"),
|
|
1477
|
+
]),
|
|
1478
|
+
flags: new Set([
|
|
1479
|
+
...["-s", "-a", "-c"],
|
|
1480
|
+
...long("exclude-node-modules", "auto-wrap", "es-modules", "compact", "preserve-comments"),
|
|
1481
|
+
...long("produce-source-map", "source-map", "instrument", "exclude-after-remap", "per-file"),
|
|
1482
|
+
...long("check-coverage", "show-process-tree", "skip-empty", "skip-full", "silent", "all"),
|
|
1483
|
+
...long("eager", "cache", "babel-cache", "use-spawn-wrap", "hook-require", "clean"),
|
|
1484
|
+
...long("hook-run-in-context", "hook-run-in-this-context", "in-place", "exit-on-error"),
|
|
1485
|
+
...long("delete", "complete-copy", "no-clean", "no-cache", "no-instrument"),
|
|
1486
|
+
]),
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
/**
|
|
1490
|
+
* dotenv-cli's options, `dotenv [-e <path>] [-v <name>=<value>] [-c [env]] [-- command]`,
|
|
1491
|
+
* https://github.com/entropitor/dotenv-cli#usage. minimist reads a flag's next word as its
|
|
1492
|
+
* value, so only `-e`, `-v` and `-c` are read where no `--` stands; `-p` and `--debug` run nothing.
|
|
1493
|
+
*/
|
|
1494
|
+
const DOTENV: Options = {
|
|
1495
|
+
valued: new Set(["-e", "-v", "-c"]),
|
|
1496
|
+
flags: new Set(["-o", "--override", "--no-expand", "-q"]),
|
|
1497
|
+
optional: new Set(["--quiet"]),
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
/** dotenv-cli, reading its flags only before a `--`. */
|
|
1501
|
+
const dotenv: Reader = (words) =>
|
|
1502
|
+
words.some((word) => word.value === "--")
|
|
1503
|
+
? afterOptions(DOTENV, true)(words)
|
|
1504
|
+
: afterOptions({ valued: DOTENV.valued, flags: new Set() })(words);
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* env-cmd's options, `env-cmd [options] -- <command> [...args]`, read by commander, which
|
|
1508
|
+
* clusters short options: https://github.com/toddbluhm/env-cmd#-help. `-e` takes every word
|
|
1509
|
+
* after it that starts with no `-`, so it is read only before a `--`.
|
|
1510
|
+
*/
|
|
1511
|
+
const ENV_CMD: Options = {
|
|
1512
|
+
valued: new Set(["-f", "--file"]),
|
|
1513
|
+
flags: new Set([
|
|
1514
|
+
"-x",
|
|
1515
|
+
...long("expand-envs", "recursive", "fallback", "no-override", "silent", "use-shell"),
|
|
1516
|
+
"--verbose",
|
|
1517
|
+
]),
|
|
1518
|
+
attached: true,
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
/** env-cmd, whose `-e` and `--environments` stand only before a `--`. */
|
|
1522
|
+
const envCmd: Reader = (words) =>
|
|
1523
|
+
words.some((word) => word.value === "--")
|
|
1524
|
+
? afterOptions(
|
|
1525
|
+
{ ...ENV_CMD, valued: new Set([...ENV_CMD.valued, "-e", "--environments"]) },
|
|
1526
|
+
true,
|
|
1527
|
+
)(words)
|
|
1528
|
+
: afterOptions(ENV_CMD)(words);
|
|
1529
|
+
|
|
1530
|
+
/** env-cmd's `--use-shell` hands its command's words, joined by blanks, to a shell. */
|
|
1531
|
+
function envCmdLines(words: readonly Word[]): Lines {
|
|
1532
|
+
const reading = envCmd(words);
|
|
1533
|
+
return reading?.kind === "command" && words.some((word) => word.value === "--use-shell")
|
|
1534
|
+
? { at: new Map(), input: false, joined: [[reading.at, words.length]] }
|
|
1535
|
+
: NO_LINES;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* `dotenvx run [options] -- <command>`, https://dotenvx.com/docs/advanced/run, whose command
|
|
1540
|
+
* stands after `--`: https://github.com/dotenvx/dotenvx/blob/main/src/cli/dotenvx.js
|
|
1541
|
+
*/
|
|
1542
|
+
const DOTENVX_RUN: Options = {
|
|
1543
|
+
valued: new Set([
|
|
1544
|
+
...["-e", "-f", "-fk"],
|
|
1545
|
+
...long("env", "env-file", "env-keys-file", "convention", "ignore", "token"),
|
|
1546
|
+
]),
|
|
1547
|
+
flags: new Set(["-o", ...long("overload", "strict", "no-armor", "no-native")]),
|
|
1548
|
+
attached: true,
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
/** dotenvx's own options before its subcommand: https://dotenvx.com/docs/cli */
|
|
1552
|
+
const DOTENVX: Options = {
|
|
1553
|
+
valued: new Set(["-l", "--log-level"]),
|
|
1554
|
+
flags: new Set([
|
|
1555
|
+
"-q",
|
|
1556
|
+
"-v",
|
|
1557
|
+
"-d",
|
|
1558
|
+
"-V",
|
|
1559
|
+
"-h",
|
|
1560
|
+
...long("quiet", "verbose", "debug", "version", "help"),
|
|
1561
|
+
]),
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* cross-env-shell, which runs its words after the assignments, joined by blanks, as one
|
|
1566
|
+
* command line: https://github.com/kentcdodds/cross-env#cross-env-vs-cross-env-shell
|
|
1567
|
+
*/
|
|
1568
|
+
function crossEnvShellLines(words: readonly Word[]): Lines {
|
|
1569
|
+
const at = words.findIndex((word) => !/^\w+=/u.test(startOf(word)));
|
|
1570
|
+
return at < 0 ? NO_LINES : { at: new Map(), input: false, joined: [[at, words.length]] };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/** The row of each tool, by the name its program word's last segment writes, but its grammar. */
|
|
1574
|
+
const ROWS: ReadonlyMap<string, Row> = new Map<string, Row>([
|
|
1575
|
+
["node", runs(NODE)],
|
|
1576
|
+
["tsx", runs(TSX)],
|
|
1577
|
+
["ts-node", runs(TS_NODE)],
|
|
1578
|
+
["bun", { read: bun, runtime: BUN, launches: { after: new Set(["x"]) } }],
|
|
1579
|
+
["deno", runs(DENO)],
|
|
1580
|
+
["env", launcher(env, [], envLines)],
|
|
1581
|
+
["cross-env", launcher(crossEnv)],
|
|
1582
|
+
["cross-env-shell", launcher(undefined, [], crossEnvShellLines)],
|
|
1583
|
+
["c8", launcher(afterOptions(C8))],
|
|
1584
|
+
["nyc", launcher(afterOptions(NYC))],
|
|
1585
|
+
["dotenv", launcher(dotenv)],
|
|
1586
|
+
["env-cmd", launcher(envCmd, [], envCmdLines)],
|
|
1587
|
+
[
|
|
1588
|
+
"dotenvx",
|
|
1589
|
+
launcher(
|
|
1590
|
+
subcommands(new Map([["run", afterOptions(DOTENVX_RUN, true)]]), DOTENVX),
|
|
1591
|
+
["run"],
|
|
1592
|
+
undefined,
|
|
1593
|
+
DOTENVX,
|
|
1594
|
+
),
|
|
1595
|
+
],
|
|
1596
|
+
[
|
|
1597
|
+
"npx",
|
|
1598
|
+
{
|
|
1599
|
+
...launcher(npx),
|
|
1600
|
+
// `-c` takes a command line, which is read as a configuration file's name as well, so
|
|
1601
|
+
// every configuration `fixtures/commands/observed.json` records stays named.
|
|
1602
|
+
configurations: {
|
|
1603
|
+
valued: new Set(["-c"]),
|
|
1604
|
+
beforeOperand: new Set(["--package", "-p", "--call"]),
|
|
1605
|
+
},
|
|
1606
|
+
},
|
|
1607
|
+
],
|
|
1608
|
+
// The readers of npm and pnpm leave their own options before the subcommand unread, as each
|
|
1609
|
+
// of them, `--workspace` and `--filter` among them, may run the command in another directory.
|
|
1610
|
+
[
|
|
1611
|
+
"npm",
|
|
1612
|
+
launcher(
|
|
1613
|
+
npm,
|
|
1614
|
+
["exec", "x"],
|
|
1615
|
+
subcommandLines(new Set(["exec", "x"]), optionLines(NPM_EXEC, CALL, true), NPM),
|
|
1616
|
+
NPM,
|
|
1617
|
+
),
|
|
1618
|
+
],
|
|
1619
|
+
// pnpm and yarn run a package's command of a name that is none of theirs:
|
|
1620
|
+
// https://pnpm.io/pnpm-cli, https://yarnpkg.com/cli/run; `yarn exec` runs its command in the
|
|
1621
|
+
// workspace's root, https://yarnpkg.com/cli/exec
|
|
1622
|
+
["pnpm", launcher(pnpm, [], pnpmLines)],
|
|
1623
|
+
["pnpx", launcher(pnpmDlx, [], (words) => dlxLines(words))],
|
|
1624
|
+
["yarn", launcher(yarn)],
|
|
1625
|
+
["bunx", launcher(bunx)],
|
|
1626
|
+
// The shell's `exec`, `command` and `builtin`, and the wrappers that run their operands.
|
|
1627
|
+
...["exec", "command", "builtin", "nohup", "nice", "time", "timeout", "sudo"].map(
|
|
1628
|
+
(name) => [name, launcher(undefined)] as const,
|
|
1629
|
+
),
|
|
1630
|
+
["eval", { launches: { lines: evalLines } }],
|
|
1631
|
+
["concurrently", { launches: { lines: concurrentlyLines } }],
|
|
1632
|
+
...[".", "source"].map(
|
|
1633
|
+
(name) => [name, { launches: { line: true, lines: sourceLines } }] as const,
|
|
1634
|
+
),
|
|
1635
|
+
...["sh", "bash", "dash", "zsh", "ksh", "ash"].map((name) => [name, SHELL] as const),
|
|
1636
|
+
["xargs", { launches: { appends: xargs } }],
|
|
1637
|
+
["find", { launches: { appends: find } }],
|
|
1638
|
+
["parallel", { launches: { appends: parallel, lines: parallelLines } }],
|
|
1639
|
+
// https://eslint.org/docs/latest/use/command-line-interface
|
|
1640
|
+
["eslint", configuredBy("-c", "--config")],
|
|
1641
|
+
// https://prettier.io/docs/cli, whose `-c` is `--check`.
|
|
1642
|
+
["prettier", configuredBy("--config")],
|
|
1643
|
+
// https://stylelint.io/user-guide/cli
|
|
1644
|
+
["stylelint", configuredBy("-c", "--config")],
|
|
1645
|
+
// https://jestjs.io/docs/cli
|
|
1646
|
+
["jest", configuredBy("-c", "--config")],
|
|
1647
|
+
// https://vitest.dev/guide/cli
|
|
1648
|
+
["vitest", configuredBy("-c", "--config")],
|
|
1649
|
+
// https://vite.dev/guide/cli
|
|
1650
|
+
["vite", configuredBy("-c", "--config")],
|
|
1651
|
+
// https://webpack.js.org/api/cli/
|
|
1652
|
+
["webpack", configuredBy("-c", "--config")],
|
|
1653
|
+
// https://mochajs.org/#command-line-usage, whose `-c` is `--color`.
|
|
1654
|
+
["mocha", configuredBy("--config")],
|
|
1655
|
+
]);
|
|
1656
|
+
|
|
1657
|
+
/** A runtime's {@link Grammar}, from the same table its reader reads by. */
|
|
1658
|
+
function runtimeGrammar(line: Runtime, doc: string): Grammar {
|
|
1659
|
+
const { valued, flags, optional } = runtimeOptions(line);
|
|
1660
|
+
const inline = new Set([...(line.inline ?? []), ...(line.runner ?? [])]);
|
|
1661
|
+
const options: Options = {
|
|
1662
|
+
valued: new Set([...valued].filter((name) => !inline.has(name))),
|
|
1663
|
+
flags: new Set([...flags].filter((name) => name !== "-" && !inline.has(name))),
|
|
1664
|
+
...(optional === undefined || optional.size === 0 ? {} : { optional }),
|
|
1665
|
+
};
|
|
1666
|
+
const one: Grammar = { options, operand: "file", inline, doc };
|
|
1667
|
+
const modes = {
|
|
1668
|
+
...(line.modes === undefined ? {} : { modes: line.modes }),
|
|
1669
|
+
...(line.modeAfter === undefined ? {} : { modeAfter: line.modeAfter }),
|
|
1670
|
+
};
|
|
1671
|
+
return {
|
|
1672
|
+
...one,
|
|
1673
|
+
...modes,
|
|
1674
|
+
subcommands: new Map([...(line.subcommands ?? [])].map((name) => [name, one])),
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/**
|
|
1679
|
+
* Each tool's {@link Grammar}: its options, the object its reader parses with, and how its
|
|
1680
|
+
* other words read, which production reads only through {@link linesOf} and the enumerated
|
|
1681
|
+
* property spells whole. Tools with no table, `sudo` and `nohup` among them, have none: after
|
|
1682
|
+
* them every later word may be a program word.
|
|
1683
|
+
*/
|
|
1684
|
+
const GRAMMARS: ReadonlyMap<string, Grammar> = new Map<string, Grammar>([
|
|
1685
|
+
["node", runtimeGrammar(NODE, "https://nodejs.org/api/cli.html")],
|
|
1686
|
+
["tsx", runtimeGrammar(TSX, "https://tsx.is/usage")],
|
|
1687
|
+
["ts-node", runtimeGrammar(TS_NODE, "https://typestrong.org/ts-node/docs/options")],
|
|
1688
|
+
["bun", runtimeGrammar(BUN, "https://bun.com/docs/cli/run")],
|
|
1689
|
+
["deno", runtimeGrammar(DENO, "https://docs.deno.com/runtime/reference/cli/run/")],
|
|
1690
|
+
[
|
|
1691
|
+
"env",
|
|
1692
|
+
{
|
|
1693
|
+
options: ENV,
|
|
1694
|
+
operand: "command",
|
|
1695
|
+
lines: new Set(["-S", "--split-string"]),
|
|
1696
|
+
doc: "https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html",
|
|
1697
|
+
},
|
|
1698
|
+
],
|
|
1699
|
+
[
|
|
1700
|
+
"npx",
|
|
1701
|
+
{
|
|
1702
|
+
options: NPX,
|
|
1703
|
+
operand: "command",
|
|
1704
|
+
lines: CALL,
|
|
1705
|
+
doc: "https://docs.npmjs.com/cli/v11/commands/npx",
|
|
1706
|
+
},
|
|
1707
|
+
],
|
|
1708
|
+
[
|
|
1709
|
+
"npm",
|
|
1710
|
+
{
|
|
1711
|
+
options: NPM,
|
|
1712
|
+
operand: "command",
|
|
1713
|
+
subcommands: new Map(
|
|
1714
|
+
["exec", "x"].map((name) => [
|
|
1715
|
+
name,
|
|
1716
|
+
{
|
|
1717
|
+
options: NPM_EXEC,
|
|
1718
|
+
operand: "command",
|
|
1719
|
+
lines: CALL,
|
|
1720
|
+
later: true,
|
|
1721
|
+
doc: "https://docs.npmjs.com/cli/v11/commands/npm-exec",
|
|
1722
|
+
},
|
|
1723
|
+
]),
|
|
1724
|
+
),
|
|
1725
|
+
doc: "https://docs.npmjs.com/cli/v11/using-npm/config",
|
|
1726
|
+
},
|
|
1727
|
+
],
|
|
1728
|
+
[
|
|
1729
|
+
"pnpm",
|
|
1730
|
+
{
|
|
1731
|
+
options: PNPM,
|
|
1732
|
+
operand: "command",
|
|
1733
|
+
subcommands: new Map([
|
|
1734
|
+
[
|
|
1735
|
+
"exec",
|
|
1736
|
+
{ options: PNPM, operand: "command", lines: PNPM_LINES, doc: "https://pnpm.io/cli/exec" },
|
|
1737
|
+
],
|
|
1738
|
+
[
|
|
1739
|
+
"dlx",
|
|
1740
|
+
{
|
|
1741
|
+
options: PNPM_DLX_OPTIONS,
|
|
1742
|
+
operand: "command",
|
|
1743
|
+
lines: PNPM_LINES,
|
|
1744
|
+
doc: "https://pnpm.io/cli/dlx",
|
|
1745
|
+
},
|
|
1746
|
+
],
|
|
1747
|
+
]),
|
|
1748
|
+
lines: PNPM_LINES,
|
|
1749
|
+
doc: "https://pnpm.io/pnpm-cli#options",
|
|
1750
|
+
},
|
|
1751
|
+
],
|
|
1752
|
+
[
|
|
1753
|
+
"yarn",
|
|
1754
|
+
{
|
|
1755
|
+
options: YARN,
|
|
1756
|
+
operand: "command",
|
|
1757
|
+
subcommands: new Map([
|
|
1758
|
+
["dlx", { options: YARN_DLX, operand: "command", doc: "https://yarnpkg.com/cli/dlx" }],
|
|
1759
|
+
]),
|
|
1760
|
+
doc: "https://yarnpkg.com/cli",
|
|
1761
|
+
},
|
|
1762
|
+
],
|
|
1763
|
+
["bunx", { options: BUNX, operand: "command", doc: "https://bun.com/docs/pm/bunx" }],
|
|
1764
|
+
[
|
|
1765
|
+
"pnpx",
|
|
1766
|
+
{
|
|
1767
|
+
options: PNPM_DLX_OPTIONS,
|
|
1768
|
+
operand: "command",
|
|
1769
|
+
lines: PNPM_LINES,
|
|
1770
|
+
doc: "https://pnpm.io/cli/dlx#pnpx",
|
|
1771
|
+
},
|
|
1772
|
+
],
|
|
1773
|
+
["c8", { options: C8, operand: "command", doc: "https://github.com/bcoe/c8#cli-options" }],
|
|
1774
|
+
["nyc", { options: NYC, operand: "command", doc: "https://github.com/istanbuljs/nyc#usage" }],
|
|
1775
|
+
// Its flags stand only before a `--`, so a form with one before no `--` caps.
|
|
1776
|
+
[
|
|
1777
|
+
"dotenv",
|
|
1778
|
+
{ options: DOTENV, operand: "command", doc: "https://github.com/entropitor/dotenv-cli#usage" },
|
|
1779
|
+
],
|
|
1780
|
+
[
|
|
1781
|
+
"env-cmd",
|
|
1782
|
+
{
|
|
1783
|
+
options: ENV_CMD,
|
|
1784
|
+
operand: "command",
|
|
1785
|
+
lines: new Set(["--use-shell"]),
|
|
1786
|
+
doc: "https://github.com/toddbluhm/env-cmd#-help",
|
|
1787
|
+
},
|
|
1788
|
+
],
|
|
1789
|
+
[
|
|
1790
|
+
"dotenvx",
|
|
1791
|
+
{
|
|
1792
|
+
options: DOTENVX,
|
|
1793
|
+
operand: "command",
|
|
1794
|
+
subcommands: new Map([
|
|
1795
|
+
[
|
|
1796
|
+
"run",
|
|
1797
|
+
{
|
|
1798
|
+
options: DOTENVX_RUN,
|
|
1799
|
+
operand: "command",
|
|
1800
|
+
ended: true,
|
|
1801
|
+
doc: "https://dotenvx.com/docs/advanced/run",
|
|
1802
|
+
},
|
|
1803
|
+
],
|
|
1804
|
+
]),
|
|
1805
|
+
doc: "https://dotenvx.com/docs",
|
|
1806
|
+
},
|
|
1807
|
+
],
|
|
1808
|
+
...["sh", "bash", "dash", "zsh", "ksh", "ash"].map(
|
|
1809
|
+
(name) =>
|
|
1810
|
+
[
|
|
1811
|
+
name,
|
|
1812
|
+
{
|
|
1813
|
+
options: SHELL_OPTIONS,
|
|
1814
|
+
operand: "line",
|
|
1815
|
+
lines: SHELL_LINES,
|
|
1816
|
+
doc: "https://pubs.opengroup.org/onlinepubs/9799919799/utilities/sh.html",
|
|
1817
|
+
},
|
|
1818
|
+
] as const,
|
|
1819
|
+
),
|
|
1820
|
+
[
|
|
1821
|
+
"concurrently",
|
|
1822
|
+
{
|
|
1823
|
+
options: CONCURRENTLY,
|
|
1824
|
+
operand: "lines",
|
|
1825
|
+
lines: new Set([TEARDOWN]),
|
|
1826
|
+
doc: "https://github.com/open-cli-tools/concurrently/blob/main/bin/index.ts",
|
|
1827
|
+
},
|
|
1828
|
+
],
|
|
1829
|
+
[
|
|
1830
|
+
"xargs",
|
|
1831
|
+
{
|
|
1832
|
+
options: XARGS,
|
|
1833
|
+
operand: "appended",
|
|
1834
|
+
doc: "https://www.gnu.org/software/findutils/manual/html_node/find_html/xargs-options.html",
|
|
1835
|
+
},
|
|
1836
|
+
],
|
|
1837
|
+
[
|
|
1838
|
+
"parallel",
|
|
1839
|
+
{
|
|
1840
|
+
options: PARALLEL,
|
|
1841
|
+
operand: "appended",
|
|
1842
|
+
doc: "https://www.gnu.org/software/parallel/parallel.html",
|
|
1843
|
+
},
|
|
1844
|
+
],
|
|
1845
|
+
]);
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* The row of each tool, by the name its program word's last segment writes, with its grammar,
|
|
1849
|
+
* which the enumerated property of `text/named-files.property.test.ts` spells every option of.
|
|
1850
|
+
*/
|
|
1851
|
+
export const TOOLS: ReadonlyMap<string, Row> = new Map(
|
|
1852
|
+
[...ROWS].map(([name, row]) => {
|
|
1853
|
+
const grammar = GRAMMARS.get(name);
|
|
1854
|
+
return [name, grammar === undefined ? row : { ...row, grammar }];
|
|
1855
|
+
}),
|
|
1856
|
+
);
|
|
1857
|
+
|
|
1858
|
+
/** Where the words after the program word of `row` write the command lines it hands to a shell. */
|
|
1859
|
+
function linesOf(row: Row | undefined): ((words: readonly Word[]) => Lines) | undefined {
|
|
1860
|
+
const grammar = row?.grammar;
|
|
1861
|
+
return (
|
|
1862
|
+
row?.launches?.lines ??
|
|
1863
|
+
(grammar?.lines === undefined || row?.launches === undefined
|
|
1864
|
+
? undefined
|
|
1865
|
+
: optionLines(grammar.options, grammar.lines, grammar.later))
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* The row of a simple command's program word, by the name its last path segment writes, as a
|
|
1871
|
+
* package runner's operand where `packaged`, its version removed.
|
|
1872
|
+
*/
|
|
1873
|
+
function rowOf(program: Word | undefined, packaged = false): Row | undefined {
|
|
1874
|
+
const name = programName(program)?.replace(/\.(?:exe|cmd)$/iu, "");
|
|
1875
|
+
return name === undefined ? undefined : TOOLS.get(commandName(name, packaged));
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
* The last path segment of a program word, a dynamic one's too where it writes that segment
|
|
1880
|
+
* whole after a `/`, as `"$(npm root)/.bin/tsx"` does, or `undefined` where the run computes it.
|
|
1881
|
+
*/
|
|
1882
|
+
function programName(program: Word | undefined): string | undefined {
|
|
1883
|
+
const text = program?.value ?? program?.written;
|
|
1884
|
+
const last = text?.slice(text.lastIndexOf("/") + 1);
|
|
1885
|
+
return last === undefined ||
|
|
1886
|
+
last.includes(PART) ||
|
|
1887
|
+
(program?.value === undefined && last === text)
|
|
1888
|
+
? undefined
|
|
1889
|
+
: last;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/** What the reader of one simple command's program reads it as, or `undefined` where none understands it. */
|
|
1893
|
+
export function readTool(words: readonly Word[]): Reading | undefined {
|
|
1894
|
+
const reading = readRow(words);
|
|
1895
|
+
return reading?.kind === "unknown" ? undefined : reading;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/** The command each launcher of `words` hands on to in turn, as {@link readTool} reads it, and what its last one is read as. */
|
|
1899
|
+
export function handedOn(
|
|
1900
|
+
words: readonly Word[],
|
|
1901
|
+
): readonly [readonly Word[], Exclude<Reading, { readonly kind: "command" }> | undefined] {
|
|
1902
|
+
let handed = words;
|
|
1903
|
+
let reading = readTool(handed);
|
|
1904
|
+
while (reading?.kind === "command") {
|
|
1905
|
+
handed = reading.words;
|
|
1906
|
+
reading = readTool(handed);
|
|
1907
|
+
}
|
|
1908
|
+
return [handed, reading];
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
/** What one simple command writes to its standard output, by the places of its words, the program word's 0. */
|
|
1912
|
+
interface Output {
|
|
1913
|
+
/** The places of the words each written as one line, as `cat`'s operands name files. */
|
|
1914
|
+
readonly at: readonly number[];
|
|
1915
|
+
/** The places, from one word up to before another, whose words joined by blanks are one line. */
|
|
1916
|
+
readonly joined: readonly (readonly [number, number])[];
|
|
1917
|
+
/** The lines it writes that no word writes whole. */
|
|
1918
|
+
readonly texts: readonly string[];
|
|
1919
|
+
/** Whether it writes its own standard input too. */
|
|
1920
|
+
readonly input: boolean;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
/** 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 */
|
|
1924
|
+
const ECHO_OPTION = /^-[neE]+$/u;
|
|
1925
|
+
|
|
1926
|
+
/** The escapes of a `printf` format this reading writes, each the text it stands for; any other is unread. */
|
|
1927
|
+
const PRINTF_ESCAPES: ReadonlyMap<string, string> = new Map([
|
|
1928
|
+
["\\n", "\n"],
|
|
1929
|
+
["\\t", "\t"],
|
|
1930
|
+
["\\\\", "\\"],
|
|
1931
|
+
["%%", "%"],
|
|
1932
|
+
]);
|
|
1933
|
+
|
|
1934
|
+
/**
|
|
1935
|
+
* What one simple command writes to its standard output, `undefined` where no row reads it,
|
|
1936
|
+
* another program's output among it. `echo` writes its operands joined by blanks as one line,
|
|
1937
|
+
* `undefined` where one holds a `\` or `-e` stands, as XSI echo reads escapes:
|
|
1938
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/echo.html. `printf` writes its
|
|
1939
|
+
* format, `undefined` where it holds a conversion or an escape other than `%%`, `\n`, `\t` and
|
|
1940
|
+
* `\\`, or other operands follow it, whose output POSIX leaves unspecified:
|
|
1941
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/printf.html. `cat` writes the file
|
|
1942
|
+
* each operand names and, with none or a `-`, its own standard input:
|
|
1943
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/cat.html.
|
|
1944
|
+
*/
|
|
1945
|
+
export function outputOf(words: readonly Word[]): Output | undefined {
|
|
1946
|
+
const name = programName(words[0]);
|
|
1947
|
+
const none: Output = { at: [], joined: [], texts: [], input: false };
|
|
1948
|
+
if (name === "echo") {
|
|
1949
|
+
let from = 1;
|
|
1950
|
+
while (ECHO_OPTION.test(words[from]?.value ?? "")) {
|
|
1951
|
+
from += 1;
|
|
1952
|
+
}
|
|
1953
|
+
const escaped =
|
|
1954
|
+
words.slice(1, from).some((word) => word.value?.includes("e") === true) ||
|
|
1955
|
+
words.slice(from).some((word) => (word.value ?? word.written ?? word.text).includes("\\"));
|
|
1956
|
+
return escaped ? undefined : { ...none, joined: [[from, words.length]] };
|
|
1957
|
+
}
|
|
1958
|
+
if (name === "printf") {
|
|
1959
|
+
const operands = words.slice(words[1]?.value === "--" ? 2 : 1);
|
|
1960
|
+
const [format, ...more] = operands;
|
|
1961
|
+
const known = format?.value;
|
|
1962
|
+
if (known === undefined || known.startsWith("-") || more.length > 0) {
|
|
1963
|
+
return known === "-v" ? none : undefined;
|
|
1964
|
+
}
|
|
1965
|
+
const rest = known.replaceAll(/\\.|%./gsu, (escape) =>
|
|
1966
|
+
PRINTF_ESCAPES.has(escape) ? "" : "\0",
|
|
1967
|
+
);
|
|
1968
|
+
return rest.includes("\\") || rest.includes("%") || rest.includes("\0")
|
|
1969
|
+
? undefined
|
|
1970
|
+
: {
|
|
1971
|
+
...none,
|
|
1972
|
+
texts: [known.replaceAll(/\\.|%./gsu, (escape) => PRINTF_ESCAPES.get(escape) ?? escape)],
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (name !== "cat") {
|
|
1976
|
+
return undefined;
|
|
1977
|
+
}
|
|
1978
|
+
const operands = words.flatMap((word, at) =>
|
|
1979
|
+
at > 0 && !/^-./u.test(word.value ?? "") ? [at] : [],
|
|
1980
|
+
);
|
|
1981
|
+
return {
|
|
1982
|
+
...none,
|
|
1983
|
+
at: operands.filter((at) => words[at]?.value !== "-"),
|
|
1984
|
+
input: operands.length === 0 || operands.some((at) => words[at]?.value === "-"),
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/** What the reader of one simple command's program reads it as, {@link UNKNOWN} where its table does not model it. */
|
|
1989
|
+
function readRow(words: readonly Word[]): Reading | Unknown | undefined {
|
|
1990
|
+
const [program, ...rest] = words;
|
|
1991
|
+
return after(1, rowOf(program)?.read?.(rest));
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
/** The files a runtime's test runner runs: the patterns its operands write, and whether it strips types. */
|
|
1995
|
+
export interface TestRun {
|
|
1996
|
+
readonly patterns: readonly string[];
|
|
1997
|
+
readonly stripped: boolean;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* The test run of one simple command whose program word names a runtime, as `node --test`, the
|
|
2002
|
+
* one reading of its options that tests.1 roots its files by, or `undefined` where it runs no
|
|
2003
|
+
* test runner, its table does not model it or a word it needs is dynamic.
|
|
2004
|
+
*/
|
|
2005
|
+
export function testRun(words: readonly Word[]): TestRun | undefined {
|
|
2006
|
+
const [program, ...rest] = words;
|
|
2007
|
+
const line = rowOf(program)?.runtime;
|
|
2008
|
+
const read = line === undefined ? undefined : runtimeLine(line, rest);
|
|
2009
|
+
if (read === undefined || isUnknown(read) || read.tests === undefined) {
|
|
2010
|
+
return undefined;
|
|
2011
|
+
}
|
|
2012
|
+
const patterns = rest.slice(read.tests).map((word) => word.value);
|
|
2013
|
+
return patterns.every((one) => one !== undefined)
|
|
2014
|
+
? {
|
|
2015
|
+
patterns,
|
|
2016
|
+
stripped:
|
|
2017
|
+
!read.named.has("--no-strip-types") && !read.named.has("--no-experimental-strip-types"),
|
|
2018
|
+
}
|
|
2019
|
+
: undefined;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/** Whether `input`, the file a `<` names, writes no path, so a reader of it runs a file no word writes. */
|
|
2023
|
+
function unnamed(input: Word | undefined): boolean {
|
|
2024
|
+
return input !== undefined && input.value === undefined && !writesPath(input.written);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* The launch of the program word `program`, at `at` of `words`, where it may run a command
|
|
2029
|
+
* written after it: after one of its subcommands, past its own options, or a dynamic word or an
|
|
2030
|
+
* unread option that may be one, and for a shell where its words write a command line.
|
|
2031
|
+
*/
|
|
2032
|
+
function launchAt(words: readonly Word[], at: number, program: Word): Launch | undefined {
|
|
2033
|
+
const launch = rowOf(program)?.launches;
|
|
2034
|
+
if (launch?.after !== undefined) {
|
|
2035
|
+
const sub = launch.options === undefined ? 0 : operandAt(words.slice(at + 1), launch.options);
|
|
2036
|
+
const next = sub === undefined ? undefined : words[at + 1 + sub];
|
|
2037
|
+
return sub === undefined ||
|
|
2038
|
+
(next !== undefined && (next.value === undefined || launch.after.has(next.value)))
|
|
2039
|
+
? launch
|
|
2040
|
+
: undefined;
|
|
2041
|
+
}
|
|
2042
|
+
return launch?.line !== true || (launch.lines?.(words.slice(at + 1)).at.size ?? 0) > 0
|
|
2043
|
+
? launch
|
|
2044
|
+
: undefined;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
/** The program words of one simple command, as {@link chainOf} reads them. */
|
|
2048
|
+
interface Chain {
|
|
2049
|
+
/** Each static program word, by its place: the first word, and each one a launcher hands on. */
|
|
2050
|
+
readonly programs: ReadonlyMap<number, Word>;
|
|
2051
|
+
/** The place of the first dynamic word that may be a program word. */
|
|
2052
|
+
readonly dynamic: number | undefined;
|
|
2053
|
+
/** The words, each holding a text a launcher replaces with a path it finds dynamic, that text a part. */
|
|
2054
|
+
readonly words: readonly Word[];
|
|
2055
|
+
/** Each such text, by the place of the words it may stand in. */
|
|
2056
|
+
readonly replaced: ReadonlyMap<number, RegExp>;
|
|
2057
|
+
/** Where the words of an appending launcher's command end, as at `parallel`'s `:::`. */
|
|
2058
|
+
readonly end: number | undefined;
|
|
2059
|
+
/** The place of the first appending launcher with an option or a construct no row reads. */
|
|
2060
|
+
readonly unread: number | undefined;
|
|
2061
|
+
/** Whether a launcher's reader reads a word its table does not model, {@link UNKNOWN}. */
|
|
2062
|
+
readonly unknown: boolean;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* The program words of `words`, a simple command: the first word, the program word each
|
|
2067
|
+
* launcher hands a command on to, as its reader reads it or, for one that hands its commands
|
|
2068
|
+
* the paths it finds, where each starts after its options, and, after a launcher whose words
|
|
2069
|
+
* its reader does not read or that has no reader, each later word up to the first dynamic one.
|
|
2070
|
+
*/
|
|
2071
|
+
function chainOf(words: readonly Word[]): Chain {
|
|
2072
|
+
const read = [...words];
|
|
2073
|
+
const replaced = new Map<number, RegExp>();
|
|
2074
|
+
const programs = new Map<number, Word>();
|
|
2075
|
+
let dynamic: number | undefined;
|
|
2076
|
+
let end: number | undefined;
|
|
2077
|
+
let unread: number | undefined;
|
|
2078
|
+
let unknown = false;
|
|
2079
|
+
const scanned = new Set<number>();
|
|
2080
|
+
const queue: (readonly [number, Word | undefined])[] = [[0, read[0]]];
|
|
2081
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
2082
|
+
const [at, program] = next;
|
|
2083
|
+
if (program === undefined || programs.has(at)) {
|
|
2084
|
+
continue;
|
|
2085
|
+
}
|
|
2086
|
+
if (programName(program) === undefined) {
|
|
2087
|
+
dynamic = Math.min(dynamic ?? at, at);
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
programs.set(at, program);
|
|
2091
|
+
const launch = launchAt(read, at, program);
|
|
2092
|
+
const appending = launch?.appends?.(read.slice(at + 1));
|
|
2093
|
+
const pattern = appending?.replaced;
|
|
2094
|
+
for (
|
|
2095
|
+
let later = at + 1 + (appending?.from ?? read.length);
|
|
2096
|
+
pattern !== undefined && later < read.length;
|
|
2097
|
+
later += 1
|
|
2098
|
+
) {
|
|
2099
|
+
const word = read[later];
|
|
2100
|
+
const written = word?.written?.replace(pattern, PART);
|
|
2101
|
+
replaced.set(later, pattern);
|
|
2102
|
+
if (word !== undefined && written !== undefined && written !== word.written) {
|
|
2103
|
+
read[later] = { value: undefined, text: word.text, written };
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
if (appending?.end !== undefined) {
|
|
2107
|
+
end = Math.min(end ?? read.length, at + 1 + appending.end);
|
|
2108
|
+
}
|
|
2109
|
+
if (appending !== undefined && appending.starts === undefined) {
|
|
2110
|
+
unread = Math.min(unread ?? at, at);
|
|
2111
|
+
}
|
|
2112
|
+
const reading =
|
|
2113
|
+
launch === undefined || appending !== undefined
|
|
2114
|
+
? undefined
|
|
2115
|
+
: readRow([program, ...read.slice(at + 1)]);
|
|
2116
|
+
unknown ||= reading?.kind === "unknown";
|
|
2117
|
+
if (appending?.starts !== undefined) {
|
|
2118
|
+
queue.push(
|
|
2119
|
+
...appending.starts.map((start) => [at + 1 + start, read[at + 1 + start]] as const),
|
|
2120
|
+
);
|
|
2121
|
+
} else if (reading?.kind === "command") {
|
|
2122
|
+
queue.push([at + reading.at, reading.words[0]]);
|
|
2123
|
+
} else if (launch !== undefined && (reading === undefined || reading.kind === "unknown")) {
|
|
2124
|
+
// An earlier scan that reached `later` queued every word from there to the same stop.
|
|
2125
|
+
for (let later = at + 1; later < read.length && !scanned.has(later); later += 1) {
|
|
2126
|
+
scanned.add(later);
|
|
2127
|
+
queue.push([later, read[later]]);
|
|
2128
|
+
if (programName(read[later]) === undefined) {
|
|
2129
|
+
break;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return { programs, dynamic, words: read, replaced, end, unread, unknown };
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/** A character a glob reads specially, so a pattern holding one names more than the path it writes. */
|
|
2138
|
+
const GLOB = /[*?[\]{}]/u;
|
|
2139
|
+
|
|
2140
|
+
/** What of one simple command may write the file a command runs, read by {@link fileOperands}. */
|
|
2141
|
+
interface FileOperands {
|
|
2142
|
+
/** The places of the words that may write it. */
|
|
2143
|
+
readonly places: ReadonlySet<number>;
|
|
2144
|
+
/** Whether it may run a file no word of the command writes. */
|
|
2145
|
+
readonly unwritten: boolean;
|
|
2146
|
+
/** The places of the words it hands a shell as a command line, each with the option text it starts with. */
|
|
2147
|
+
readonly lines: ReadonlyMap<number, string>;
|
|
2148
|
+
/** Whether it reads command lines from its standard input. */
|
|
2149
|
+
readonly input: boolean;
|
|
2150
|
+
/** Whether a runtime runs its standard input as its entry, as `node` given no file does. */
|
|
2151
|
+
readonly stdin: boolean;
|
|
2152
|
+
/** The texts, by the place of each word they may stand in, a launcher replaces with each path it finds. */
|
|
2153
|
+
readonly replaced: ReadonlyMap<number, RegExp>;
|
|
2154
|
+
/** The places, each from one word up to before another, whose words joined by blanks it hands a shell as one command line. */
|
|
2155
|
+
readonly joined: readonly (readonly [number, number])[];
|
|
2156
|
+
/** The command lines it hands a shell that no word writes whole. */
|
|
2157
|
+
readonly texts: readonly string[];
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
/**
|
|
2161
|
+
* The words of one simple command that may write the file a runtime runs (roots.13): where
|
|
2162
|
+
* `program` holds, a dynamic program word and every word after it; after each program word
|
|
2163
|
+
* {@link chainOf} places that names a runtime, its entry, or every later word its reader does
|
|
2164
|
+
* not read. A runtime with no entry after a tool that hands it the paths it finds, a command a
|
|
2165
|
+
* row's table does not model, or one that reads command lines or its standard input from the
|
|
2166
|
+
* 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
|
|
2167
|
+
* pattern may write a file, and a dynamic or glob one, or none, runs a file no word writes.
|
|
2168
|
+
*/
|
|
2169
|
+
export function fileOperands(
|
|
2170
|
+
words: readonly Word[],
|
|
2171
|
+
program: boolean,
|
|
2172
|
+
input?: Word,
|
|
2173
|
+
tests = false,
|
|
2174
|
+
): FileOperands {
|
|
2175
|
+
const chain = chainOf(words);
|
|
2176
|
+
const read = chain.words;
|
|
2177
|
+
const places = new Set<number>();
|
|
2178
|
+
const fromHere = (at: number): void => {
|
|
2179
|
+
for (let later = at; later < read.length; later += 1) {
|
|
2180
|
+
places.add(later);
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
if (program && chain.dynamic !== undefined) {
|
|
2184
|
+
fromHere(chain.dynamic);
|
|
2185
|
+
}
|
|
2186
|
+
let unwritten = chain.unknown || chain.unread !== undefined;
|
|
2187
|
+
let reads = false;
|
|
2188
|
+
let stdin = false;
|
|
2189
|
+
const programs = [...chain.programs].sort(([one], [other]) => one - other);
|
|
2190
|
+
const [handed] = tests ? handedOn(words) : [[]];
|
|
2191
|
+
// Where the command reader's own test run is this runtime's, tests.1 roots its files.
|
|
2192
|
+
const rooted =
|
|
2193
|
+
testRun(handed) !== undefined &&
|
|
2194
|
+
handed.every((word, at) => read[read.length - handed.length + at]?.value === word.value)
|
|
2195
|
+
? read.length - handed.length
|
|
2196
|
+
: -1;
|
|
2197
|
+
const appender =
|
|
2198
|
+
programs.find(([, word]) => rowOf(word)?.launches?.appends !== undefined)?.[0] ?? -1;
|
|
2199
|
+
for (const [named, word] of programs) {
|
|
2200
|
+
const line = rowOf(word, true)?.runtime;
|
|
2201
|
+
if (line === undefined) {
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
const end = chain.end !== undefined && named < chain.end ? chain.end : read.length;
|
|
2205
|
+
const runtime = runtimeLine(line, read.slice(named + 1, end));
|
|
2206
|
+
if (runtime === undefined || isUnknown(runtime)) {
|
|
2207
|
+
fromHere(named + 1);
|
|
2208
|
+
unwritten ||= isUnknown(runtime);
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
stdin ||= runtime.stdin;
|
|
2212
|
+
if (runtime.tests !== undefined && named !== rooted) {
|
|
2213
|
+
const from = named + 1 + runtime.tests;
|
|
2214
|
+
const patterns = read.slice(from, end);
|
|
2215
|
+
patterns.forEach((_, at) => places.add(from + at));
|
|
2216
|
+
unwritten ||=
|
|
2217
|
+
patterns.length === 0 ||
|
|
2218
|
+
patterns.some((one) => one.value === undefined || GLOB.test(one.value));
|
|
2219
|
+
}
|
|
2220
|
+
if (runtime.entry === undefined) {
|
|
2221
|
+
unwritten ||= appender >= 0 && appender < named;
|
|
2222
|
+
} else {
|
|
2223
|
+
places.add(named + 1 + runtime.entry);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
const lines = new Map<number, string>();
|
|
2227
|
+
const replaced = new Map(chain.replaced);
|
|
2228
|
+
const joined: (readonly [number, number])[] = [];
|
|
2229
|
+
const texts: string[] = [];
|
|
2230
|
+
for (const [at, word] of programs) {
|
|
2231
|
+
const found = linesOf(rowOf(word))?.(read.slice(at + 1)) ?? NO_LINES;
|
|
2232
|
+
for (const [one, prefix] of found.at) {
|
|
2233
|
+
lines.set(at + 1 + one, prefix);
|
|
2234
|
+
if (found.replaced !== undefined) {
|
|
2235
|
+
replaced.set(at + 1 + one, found.replaced);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
joined.push(...(found.joined ?? []).map(([from, to]) => [at + 1 + from, at + 1 + to] as const));
|
|
2239
|
+
texts.push(...(found.texts ?? []));
|
|
2240
|
+
reads ||= found.input;
|
|
2241
|
+
unwritten ||=
|
|
2242
|
+
found.unwritten === true || (found.appended === true && appender >= 0 && appender < at);
|
|
2243
|
+
}
|
|
2244
|
+
unwritten ||= (reads || stdin) && unnamed(input);
|
|
2245
|
+
return { places, unwritten, lines, input: reads, stdin, replaced, joined, texts };
|
|
476
2246
|
}
|
|
477
2247
|
|
|
478
2248
|
/**
|