@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19

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