@cplieger/deadset-ts 8.0.0-dev.2 → 8.0.0-dev.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/dist/bin/mapper-connection.js +2 -1
- package/dist/bin/node-host.js +24 -5
- package/dist/src/alias-chain.js +2 -7
- package/dist/src/amd.js +81 -0
- package/dist/src/analysis.js +269 -432
- package/dist/src/augmentations.js +64 -3
- package/dist/src/automatic-types.js +10 -29
- package/dist/src/bindings.js +382 -0
- package/dist/src/calls.js +279 -43
- package/dist/src/commands/held.js +499 -0
- package/dist/src/commands/read.js +161 -0
- package/dist/src/commands/shell.js +169 -0
- package/dist/src/commands/tools.js +1847 -0
- package/dist/src/component-files.js +3 -2
- package/dist/src/component-layer.js +4 -2
- package/dist/src/config.js +6 -82
- package/dist/src/conformance-record.js +1 -1
- package/dist/src/consumers.js +10 -38
- package/dist/src/contextual-uses.js +57 -26
- package/dist/src/contract/config.generated.js +561 -0
- package/dist/src/contract/contract.generated.js +1369 -0
- package/dist/src/contract/report.generated.js +866 -0
- package/dist/src/dependencies.js +164 -121
- package/dist/src/destructuring.js +1 -19
- package/dist/src/edges.js +38 -122
- package/dist/src/element-access.js +181 -168
- package/dist/src/entries/build-scripts.js +222 -0
- package/dist/src/{configuration-files.js → entries/configuration-files.js} +52 -256
- package/dist/src/{convention-rows.js → entries/convention-rows.js} +170 -30
- package/dist/src/entries/conventions.js +740 -0
- package/dist/src/{html-entries.js → entries/html-entries.js} +12 -42
- package/dist/src/{mdx-entries.js → entries/mdx-entries.js} +5 -28
- package/dist/src/entries/run-directories.js +77 -0
- package/dist/src/entries/sources.js +144 -0
- package/dist/src/entries/static-module.js +670 -0
- package/dist/src/entries/test-runners.js +650 -0
- package/dist/src/entries/workflow-steps.js +345 -0
- package/dist/src/enum-group.js +3 -10
- package/dist/src/erased-stores.js +4 -6
- package/dist/src/exempt-classes.js +1 -63
- package/dist/src/exempt.js +21 -66
- package/dist/src/exposure.js +400 -0
- package/dist/src/extensions.js +37 -0
- package/dist/src/facts.js +38 -0
- package/dist/src/failure.js +36 -0
- package/dist/src/file-facts.js +3 -26
- package/dist/src/findings/completion.js +109 -56
- package/dist/src/findings/dependencies-and-module-machinery.js +4 -5
- package/dist/src/findings/emitters.js +27 -41
- package/dist/src/{implementations.js → findings/implementations.js} +3 -3
- package/dist/src/{in-operands.js → findings/in-operands.js} +3 -2
- package/dist/src/findings/interfaces.js +4 -5
- package/dist/src/findings/intra-function.js +16 -13
- package/dist/src/findings/non-code-artifacts.js +17 -12
- package/dist/src/findings/reads-and-writes.js +86 -27
- package/dist/src/findings/self-check.js +44 -30
- package/dist/src/findings/unused-declarations.js +29 -45
- package/dist/src/findings/visibility-narrowing.js +23 -31
- package/dist/src/findings-pass.js +13 -21
- package/dist/src/framework-lifecycle.js +46 -19
- package/dist/src/fs/document.js +52 -0
- package/dist/src/fs/manifest.js +67 -0
- package/dist/src/fs/tree.js +88 -0
- package/dist/src/generated-file.js +174 -28
- package/dist/src/git-ignore.js +39 -26
- package/dist/src/import-bindings.js +73 -28
- package/dist/src/index/sites.js +557 -0
- package/dist/src/index.js +2 -2
- package/dist/src/indexed-access.js +523 -478
- package/dist/src/injection-container.js +2 -2
- package/dist/src/interface-satisfaction.js +1 -1
- package/dist/src/intra-function-parts.js +63 -43
- package/dist/src/inventory/commonjs.js +507 -0
- package/dist/src/inventory/enumerate.js +167 -0
- package/dist/src/inventory/syntax.js +236 -0
- package/dist/src/inventory.js +323 -724
- package/dist/src/javascript-file.js +48 -0
- package/dist/src/json-document.js +198 -67
- package/dist/src/keyof-comparisons.js +126 -107
- package/dist/src/kinds.js +3 -366
- package/dist/src/{components.js → liveness/components.js} +2 -3
- package/dist/src/{graph.js → liveness/graph.js} +52 -28
- package/dist/src/liveness/matrix.js +99 -0
- package/dist/src/liveness/sweep.js +558 -0
- package/dist/src/load/classify.js +289 -0
- package/dist/src/load/diagnostics.js +87 -0
- package/dist/src/load/discover.js +285 -0
- package/dist/src/{emit-map.js → load/emit-map.js} +1 -1
- package/dist/src/load/expected-module.js +136 -0
- package/dist/src/load/load.js +27 -0
- package/dist/src/load/module-lookup.js +37 -0
- package/dist/src/load/package-scope.js +56 -0
- package/dist/src/load/plan.js +313 -0
- package/dist/src/load/specifiers.js +113 -0
- package/dist/src/load/synthesize.js +152 -0
- package/dist/src/load/test-globals.js +207 -0
- package/dist/src/load/test-joins.js +82 -0
- package/dist/src/{workspace-manifests.js → load/workspace-manifests.js} +4 -4
- package/dist/src/{workspace-programs.js → load/workspace-programs.js} +89 -66
- package/dist/src/{workspace-resolution.js → load/workspace-resolution.js} +24 -66
- package/dist/src/{workspace-run.js → load/workspace-run.js} +7 -7
- package/dist/src/{workspace.js → load/workspace.js} +62 -124
- package/dist/src/module-declarations.js +70 -85
- package/dist/src/overrides.js +134 -112
- package/dist/src/partial.js +2 -1
- package/dist/src/paths.js +82 -16
- package/dist/src/platform-calls.js +78 -0
- package/dist/src/platform-symbols.js +134 -0
- package/dist/src/position.js +43 -1
- package/dist/src/print.js +11 -79
- package/dist/src/product.js +13 -0
- package/dist/src/query.js +47 -0
- package/dist/src/ref.js +12 -20
- package/dist/src/references/read-rule.js +1 -0
- package/dist/src/references/read-rules.js +48 -0
- package/dist/src/references/sink.js +237 -0
- package/dist/src/references/sources.js +450 -0
- package/dist/src/references.js +117 -1074
- package/dist/src/report-schema.js +51 -0
- package/dist/src/report.js +57 -41
- package/dist/src/reporters/annotations.js +15 -8
- package/dist/src/reporters/sarif.js +16 -10
- package/dist/src/reporters/template.js +4 -19
- package/dist/src/resolve.js +246 -730
- package/dist/src/{configured-declarations.js → roots/configured-declarations.js} +22 -24
- package/dist/src/{entry-point-gaps.js → roots/entry-point-gaps.js} +6 -15
- package/dist/src/roots/manifest.js +216 -0
- package/dist/src/roots/published-reach.js +412 -0
- package/dist/src/roots/root-set.js +487 -0
- package/dist/src/roots/unseen-loads.js +236 -0
- package/dist/src/run.js +66 -77
- package/dist/src/schema.js +320 -343
- package/dist/src/scope.js +38 -115
- package/dist/src/serialization-contract.js +601 -22
- package/dist/src/session.js +37 -29
- package/dist/src/source-files.js +6 -10
- package/dist/src/stores.js +4 -2
- package/dist/src/suppress-file.js +25 -28
- package/dist/src/suppress-inline.js +36 -83
- package/dist/src/suppress.js +50 -71
- package/dist/src/symbol-keys.js +92 -88
- package/dist/src/template-field.js +9 -19
- package/dist/src/tests/file-class.js +76 -0
- package/dist/src/{outside-tests.js → tests/outside.js} +7 -7
- package/dist/src/{test-support.js → tests/support.js} +5 -12
- package/dist/src/text/compare.js +26 -0
- package/dist/src/text/glob.js +194 -0
- package/dist/src/text/named-files.js +298 -0
- package/dist/src/text/yaml.js +684 -0
- package/dist/src/type-errors.js +11 -232
- package/dist/src/type-parameter-reads.js +178 -139
- package/dist/src/unimplemented-callees.js +262 -242
- package/dist/src/unwrap.js +23 -0
- package/dist/src/usage.js +10 -0
- package/dist/src/vendor/unbash/ansi-c.js +132 -0
- package/dist/src/vendor/unbash/arithmetic-parts.js +37 -0
- package/dist/src/vendor/unbash/arithmetic.js +591 -0
- package/dist/src/vendor/unbash/assignment.js +131 -0
- package/dist/src/vendor/unbash/chars.js +48 -0
- package/dist/src/vendor/unbash/command.js +41 -0
- package/dist/src/vendor/unbash/expansion.js +80 -0
- package/dist/src/vendor/unbash/glob.js +268 -0
- package/dist/src/vendor/unbash/heredoc.js +27 -0
- package/dist/src/vendor/unbash/internal-types.js +1 -0
- package/dist/src/vendor/unbash/lexer.js +4274 -0
- package/dist/src/vendor/unbash/limits.js +3 -0
- package/dist/src/vendor/unbash/parser.js +1366 -0
- package/dist/src/vendor/unbash/parts.js +57 -0
- package/dist/src/vendor/unbash/substitution-context.js +8 -0
- package/dist/src/vendor/unbash/traversal.js +212 -0
- package/dist/src/vendor/unbash/types.js +1 -0
- package/dist/src/vendor/unbash/word-value.js +61 -0
- package/dist/src/vendor/unbash/word.js +40 -0
- package/dist/src/verbs/analyze.js +194 -127
- package/dist/src/verbs/describe.js +13 -18
- package/dist/src/verbs/explain.js +85 -139
- package/dist/src/verbs/print-config.js +2 -1
- package/dist/src/verbs/print-projects.js +6 -4
- package/dist/src/verbs/print-retained.js +7 -5
- package/dist/src/verbs/print-roots.js +4 -2
- package/dist/src/verbs/verb.js +1 -5
- package/dist/src/verbs/version.js +14 -3
- package/jsr.json +2 -1
- package/package.json +14 -8
- package/src/alias-chain.ts +2 -9
- package/src/amd.ts +130 -0
- package/src/analysis.ts +424 -650
- package/src/augmentations.ts +93 -4
- package/src/automatic-types.ts +12 -30
- package/src/bindings.ts +460 -0
- package/src/calls.ts +338 -46
- package/src/commands/held.ts +632 -0
- package/src/commands/read.ts +242 -0
- package/src/commands/shell.ts +228 -0
- package/src/commands/tools.ts +2382 -0
- package/src/component-files.ts +4 -2
- package/src/component-layer.ts +4 -2
- package/src/config.ts +31 -302
- package/src/conformance-record.ts +1 -1
- package/src/consumers.ts +16 -66
- package/src/contextual-uses.ts +66 -34
- package/src/contract/config.generated.ts +821 -0
- package/src/contract/contract.generated.ts +1452 -0
- package/src/contract/report.generated.ts +1241 -0
- package/src/dependencies.ts +222 -127
- package/src/destructuring.ts +1 -31
- package/src/edges.ts +41 -140
- package/src/element-access.ts +190 -187
- package/src/entries/build-scripts.ts +292 -0
- package/src/{configuration-files.ts → entries/configuration-files.ts} +68 -278
- package/src/{convention-rows.ts → entries/convention-rows.ts} +227 -31
- package/src/{conventions.ts → entries/conventions.ts} +367 -583
- package/src/{html-entries.ts → entries/html-entries.ts} +22 -48
- package/src/{mdx-entries.ts → entries/mdx-entries.ts} +7 -30
- package/src/entries/run-directories.ts +108 -0
- package/src/entries/sources.ts +266 -0
- package/src/entries/static-module.ts +858 -0
- package/src/entries/test-runners.ts +835 -0
- package/src/entries/workflow-steps.ts +432 -0
- package/src/enum-group.ts +2 -11
- package/src/erased-stores.ts +3 -7
- package/src/exempt-classes.ts +1 -75
- package/src/exempt.ts +70 -98
- package/src/exposure.ts +492 -0
- package/src/extensions.ts +45 -0
- package/src/facts.ts +46 -0
- package/src/failure.ts +63 -0
- package/src/file-facts.ts +15 -25
- package/src/finding.ts +10 -10
- package/src/findings/boundary.ts +11 -0
- package/src/findings/completion.ts +133 -64
- package/src/findings/dependencies-and-module-machinery.ts +4 -7
- package/src/findings/emitter.ts +9 -10
- package/src/findings/emitters.ts +38 -52
- package/src/{implementations.ts → findings/implementations.ts} +4 -4
- package/src/{in-operands.ts → findings/in-operands.ts} +4 -3
- package/src/findings/interfaces.ts +7 -9
- package/src/findings/intra-function.ts +16 -14
- package/src/findings/non-code-artifacts.ts +24 -15
- package/src/findings/reads-and-writes.ts +118 -41
- package/src/findings/self-check.ts +51 -38
- package/src/findings/unused-declarations.ts +31 -49
- package/src/findings/visibility-narrowing.ts +36 -40
- package/src/findings-pass.ts +15 -31
- package/src/framework-lifecycle.ts +47 -19
- package/src/fs/document.ts +70 -0
- package/src/fs/manifest.ts +96 -0
- package/src/fs/tree.ts +110 -0
- package/src/generated-file.ts +242 -35
- package/src/git-ignore.ts +50 -26
- package/src/host.ts +12 -5
- package/src/import-bindings.ts +98 -29
- package/src/index/sites.ts +781 -0
- package/src/index.ts +2 -2
- package/src/indexed-access.ts +570 -527
- package/src/injection-container.ts +2 -2
- package/src/interface-satisfaction.ts +2 -2
- package/src/intra-function-parts.ts +73 -44
- package/src/inventory/commonjs.ts +696 -0
- package/src/inventory/enumerate.ts +203 -0
- package/src/inventory/syntax.ts +311 -0
- package/src/inventory.ts +442 -895
- package/src/javascript-file.ts +54 -0
- package/src/json-document.ts +241 -76
- package/src/keyof-comparisons.ts +133 -122
- package/src/kinds.ts +5 -413
- package/src/{components.ts → liveness/components.ts} +4 -6
- package/src/{graph.ts → liveness/graph.ts} +100 -35
- package/src/liveness/matrix.ts +162 -0
- package/src/liveness/sweep.ts +855 -0
- package/src/load/classify.ts +433 -0
- package/src/load/diagnostics.ts +154 -0
- package/src/load/discover.ts +471 -0
- package/src/{emit-map.ts → load/emit-map.ts} +1 -1
- package/src/load/expected-module.ts +194 -0
- package/src/load/load.ts +73 -0
- package/src/load/module-lookup.ts +50 -0
- package/src/load/package-scope.ts +74 -0
- package/src/load/plan.ts +513 -0
- package/src/load/specifiers.ts +171 -0
- package/src/load/synthesize.ts +230 -0
- package/src/load/test-globals.ts +251 -0
- package/src/load/test-joins.ts +98 -0
- package/src/{workspace-manifests.ts → load/workspace-manifests.ts} +5 -5
- package/src/{workspace-programs.ts → load/workspace-programs.ts} +128 -85
- package/src/{workspace-resolution.ts → load/workspace-resolution.ts} +41 -77
- package/src/{workspace-run.ts → load/workspace-run.ts} +18 -16
- package/src/{workspace.ts → load/workspace.ts} +90 -134
- package/src/module-declarations.ts +74 -98
- package/src/overrides.ts +162 -134
- package/src/partial.ts +3 -2
- package/src/paths.ts +92 -16
- package/src/platform-calls.ts +119 -0
- package/src/platform-symbols.ts +189 -0
- package/src/position.ts +44 -1
- package/src/print.ts +12 -92
- package/src/product.ts +18 -0
- package/src/query.ts +92 -0
- package/src/ref.ts +12 -20
- package/src/references/read-rule.ts +60 -0
- package/src/references/read-rules.ts +55 -0
- package/src/references/sink.ts +314 -0
- package/src/references/sources.ts +632 -0
- package/src/references.ts +187 -1450
- package/src/report-schema.ts +68 -0
- package/src/report.ts +103 -226
- package/src/reporters/annotations.ts +22 -11
- package/src/reporters/json.ts +1 -1
- package/src/reporters/reporter.ts +2 -4
- package/src/reporters/reporters.ts +1 -1
- package/src/reporters/sarif.ts +29 -16
- package/src/reporters/template.ts +4 -20
- package/src/reporters/text.ts +3 -3
- package/src/resolve.ts +284 -1115
- package/src/{configured-declarations.ts → roots/configured-declarations.ts} +27 -29
- package/src/{entry-point-gaps.ts → roots/entry-point-gaps.ts} +7 -16
- package/src/roots/manifest.ts +303 -0
- package/src/roots/published-reach.ts +486 -0
- package/src/{roots.ts → roots/root-set.ts} +286 -469
- package/src/roots/unseen-loads.ts +345 -0
- package/src/run.ts +75 -91
- package/src/schema.ts +391 -388
- package/src/scope.ts +46 -138
- package/src/serialization-contract.ts +769 -23
- package/src/session.ts +87 -55
- package/src/source-files.ts +6 -11
- package/src/stores.ts +15 -3
- package/src/suppress-file.ts +24 -33
- package/src/suppress-inline.ts +55 -91
- package/src/suppress.ts +78 -79
- package/src/symbol-keys.ts +104 -107
- package/src/template-field.ts +15 -25
- package/src/tests/file-class.ts +135 -0
- package/src/{outside-tests.ts → tests/outside.ts} +16 -10
- package/src/{test-support.ts → tests/support.ts} +15 -21
- package/src/text/compare.ts +27 -0
- package/src/text/glob.ts +210 -0
- package/src/text/named-files.ts +388 -0
- package/src/text/yaml.ts +747 -0
- package/src/type-errors.ts +18 -288
- package/src/type-parameter-reads.ts +187 -158
- package/src/unimplemented-callees.ts +297 -280
- package/src/unwrap.ts +34 -0
- package/src/usage.ts +10 -0
- package/src/vendor/unbash/LICENSE +15 -0
- package/src/vendor/unbash/VENDORED.md +16 -0
- package/src/vendor/unbash/ansi-c.ts +132 -0
- package/src/vendor/unbash/arithmetic-parts.ts +53 -0
- package/src/vendor/unbash/arithmetic.ts +641 -0
- package/src/vendor/unbash/assignment.ts +163 -0
- package/src/vendor/unbash/chars.ts +48 -0
- package/src/vendor/unbash/command.ts +47 -0
- package/src/vendor/unbash/expansion.ts +144 -0
- package/src/vendor/unbash/glob.ts +284 -0
- package/src/vendor/unbash/heredoc.ts +34 -0
- package/src/vendor/unbash/internal-types.ts +94 -0
- package/src/vendor/unbash/lexer.ts +4352 -0
- package/src/vendor/unbash/limits.ts +3 -0
- package/src/vendor/unbash/parser.ts +1612 -0
- package/src/vendor/unbash/parts.ts +96 -0
- package/src/vendor/unbash/substitution-context.ts +20 -0
- package/src/vendor/unbash/traversal.ts +182 -0
- package/src/vendor/unbash/types.ts +660 -0
- package/src/vendor/unbash/word-value.ts +64 -0
- package/src/vendor/unbash/word.ts +50 -0
- package/src/verbs/analyze.ts +255 -164
- package/src/verbs/describe.ts +19 -17
- package/src/verbs/explain.ts +93 -169
- package/src/verbs/print-config.ts +2 -1
- package/src/verbs/print-projects.ts +6 -4
- package/src/verbs/print-retained.ts +7 -5
- package/src/verbs/print-roots.ts +4 -2
- package/src/verbs/verb.ts +2 -8
- package/src/verbs/version.ts +15 -3
- package/dist/src/base-url.js +0 -18
- package/dist/src/conventions.js +0 -937
- package/dist/src/discover.js +0 -468
- package/dist/src/entry-points.js +0 -529
- package/dist/src/glob.js +0 -65
- package/dist/src/manifest.js +0 -165
- package/dist/src/matrix.js +0 -246
- package/dist/src/pnpm-workspace.js +0 -120
- package/dist/src/roots.js +0 -672
- package/dist/src/setup-failure.js +0 -23
- package/dist/src/sweep.js +0 -224
- package/dist/src/test-joins.js +0 -101
- package/dist/src/version.js +0 -7
- package/dist/src/workflow-steps.js +0 -185
- package/src/base-url.ts +0 -24
- package/src/discover.ts +0 -653
- package/src/entry-points.ts +0 -687
- package/src/glob.ts +0 -67
- package/src/manifest.ts +0 -234
- package/src/matrix.ts +0 -423
- package/src/pnpm-workspace.ts +0 -135
- package/src/setup-failure.ts +0 -39
- package/src/sweep.ts +0 -347
- package/src/test-joins.ts +0 -120
- package/src/version.ts +0 -7
- package/src/workflow-steps.ts +0 -216
|
@@ -0,0 +1,1847 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layer 2 of the command reader: one row per tool, its reader of a simple command's words after
|
|
3
|
+
* its program word and the options it names its configuration file by, each as the tool
|
|
4
|
+
* documents them, and one reader per shell builtin that moves the shell's directory. A reader
|
|
5
|
+
* never guesses: an option it does not handle, or a dynamic word where it needs a value, leaves
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
import { hasURLScheme, isPathSpecifier, resolvePath } from "../paths.js";
|
|
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)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
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" };
|
|
79
|
+
}
|
|
80
|
+
if (!dynamic && options.flags.has(word)) {
|
|
81
|
+
found.push({ name: word });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!word.startsWith("-")) {
|
|
85
|
+
return { options: found, at, stop: dynamic && word === "" ? "dynamic" : "operand" };
|
|
86
|
+
}
|
|
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
|
+
};
|
|
94
|
+
}
|
|
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) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
if (value !== undefined && text !== undefined) {
|
|
121
|
+
values.set(name, [...(values.get(name) ?? []), text.slice(value.prefix.length)]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
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;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The path a URL's path text names, percent-decoded as Node's `fileURLToPath` does, with a
|
|
133
|
+
* backslash read as the URL parser reads it, or `undefined` where it does not decode or
|
|
134
|
+
* encodes a separator: https://nodejs.org/api/url.html#urlfileurltopathurl-options
|
|
135
|
+
*/
|
|
136
|
+
function urlPath(text) {
|
|
137
|
+
const path = text.replace(/[?#].*$/su, "").replaceAll("\\", "/");
|
|
138
|
+
if (/%2f|%5c/iu.test(path)) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return decodeURIComponent(path);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Where one preload is, read in `dir`: the absolute path of a file, a package's name, or
|
|
150
|
+
* `undefined` where it names neither. `require` reads a path as written; an ECMAScript
|
|
151
|
+
* specifier is a URL, so a relative one or a `file:` one is percent-decoded and any other
|
|
152
|
+
* scheme loads no file: https://nodejs.org/api/esm.html#urls
|
|
153
|
+
*/
|
|
154
|
+
export function preloadTarget(preload, dir) {
|
|
155
|
+
const { specifier, resolution } = preload;
|
|
156
|
+
if (resolution === "require") {
|
|
157
|
+
return isPathSpecifier(specifier)
|
|
158
|
+
? { path: resolvePath(dir, specifier) }
|
|
159
|
+
: hasURLScheme(specifier)
|
|
160
|
+
? undefined
|
|
161
|
+
: { package: specifier };
|
|
162
|
+
}
|
|
163
|
+
if (!isPathSpecifier(specifier) && !hasURLScheme(specifier)) {
|
|
164
|
+
return { package: specifier };
|
|
165
|
+
}
|
|
166
|
+
const local = /^file:(?:\/\/(?=\/))?/iu.exec(specifier);
|
|
167
|
+
const rest = specifier.slice(local?.[0].length ?? 0);
|
|
168
|
+
// `file://host/…` names a file on another host.
|
|
169
|
+
if ((hasURLScheme(specifier) && local === null) || rest.startsWith("//")) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const path = urlPath(rest);
|
|
173
|
+
return path === undefined ? undefined : { path: resolvePath(dir, path) };
|
|
174
|
+
}
|
|
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) {
|
|
227
|
+
return (words) => {
|
|
228
|
+
const read = runtimeLine(line, words);
|
|
229
|
+
if (isUnknown(read)) {
|
|
230
|
+
return UNKNOWN;
|
|
231
|
+
}
|
|
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)) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
kind: "run",
|
|
241
|
+
entry: entry?.value,
|
|
242
|
+
preloads: read.preloads,
|
|
243
|
+
runtime: line.runsNode && entry !== undefined ? "node" : undefined,
|
|
244
|
+
};
|
|
245
|
+
};
|
|
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"];
|
|
249
|
+
/**
|
|
250
|
+
* `node [options] [entry] [arguments]`, https://nodejs.org/api/cli.html: the options that load
|
|
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
|
+
};
|
|
300
|
+
/** `ts-node [options] [script] [arguments]`, https://typestrong.org/ts-node/docs/options; `--esm` picks no file. */
|
|
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
|
+
};
|
|
353
|
+
/** Hands on the command `words` from `at`, after the assignments `assigns` accepts. */
|
|
354
|
+
function afterAssignments(words, at, assigns) {
|
|
355
|
+
let end = at;
|
|
356
|
+
for (let word = words[end]; word !== undefined && startOf(word).includes("="); word = words[end]) {
|
|
357
|
+
if (!assigns(startOf(word))) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
end += 1;
|
|
361
|
+
}
|
|
362
|
+
const program = words[end];
|
|
363
|
+
if (program !== undefined && (program.value === undefined || program.value.startsWith("-"))) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
return program === undefined
|
|
367
|
+
? { kind: "run", entry: undefined, preloads: [], runtime: undefined }
|
|
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(" ");
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
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.
|
|
418
|
+
*/
|
|
419
|
+
const env = (words) => {
|
|
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("="))
|
|
430
|
+
? undefined
|
|
431
|
+
: afterAssignments(words, read.at, (value) => !value.startsWith("="));
|
|
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
|
+
}
|
|
438
|
+
/** `cross-env [name=value]... command [argument]...`, https://github.com/kentcdodds/cross-env#usage */
|
|
439
|
+
const crossEnv = (words) => {
|
|
440
|
+
const reading = afterAssignments(words, 0, (value) => /^\w+=/u.test(value));
|
|
441
|
+
return reading?.kind === "command" ? reading : undefined;
|
|
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
|
+
}
|
|
447
|
+
/** The command a package runner hands on: its first word a package's or a command's name. */
|
|
448
|
+
function runnerCommand(words, packaged) {
|
|
449
|
+
const [first, ...rest] = words;
|
|
450
|
+
if (first?.value === undefined || first.value.startsWith("-")) {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
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
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const NPX = packageExecOptions(true);
|
|
468
|
+
const NPM_EXEC = packageExecOptions(false);
|
|
469
|
+
const CALL = new Set(["-c", "--call"]);
|
|
470
|
+
/**
|
|
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.
|
|
474
|
+
*/
|
|
475
|
+
function packageExec(options, laterOptions) {
|
|
476
|
+
return (words) => {
|
|
477
|
+
const read = optionsOf(words, options);
|
|
478
|
+
if (read === undefined || isUnknown(read)) {
|
|
479
|
+
return read;
|
|
480
|
+
}
|
|
481
|
+
const operands = words.slice(read.at);
|
|
482
|
+
if (laterOptions && !read.ended && operands.some((word) => word.value === undefined)) {
|
|
483
|
+
return undefined;
|
|
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
|
+
}
|
|
489
|
+
const call = [...(read.values.get("-c") ?? []), ...(read.values.get("--call") ?? [])];
|
|
490
|
+
if (call.length > 0) {
|
|
491
|
+
return call.length === 1 && operands.length === 0 && call[0] !== undefined
|
|
492
|
+
? { kind: "line", text: call[0] }
|
|
493
|
+
: undefined;
|
|
494
|
+
}
|
|
495
|
+
const packages = [...(read.values.get("--package") ?? []), ...(read.values.get("-p") ?? [])];
|
|
496
|
+
return after(read.at, runnerCommand(operands, packages.length === 0));
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const npx = packageExec(NPX, false);
|
|
500
|
+
const npmExec = packageExec(NPM_EXEC, true);
|
|
501
|
+
/**
|
|
502
|
+
* `npm run <script> [-- <arguments>]`, https://docs.npmjs.com/cli/v11/commands/npm-run-script:
|
|
503
|
+
* it runs another script of the same manifest, which is read on its own.
|
|
504
|
+
*/
|
|
505
|
+
const npmRun = (words) => {
|
|
506
|
+
const [script, ...rest] = words;
|
|
507
|
+
const ended = rest.findIndex((word) => word.value === "--");
|
|
508
|
+
const before = ended < 0 ? rest : rest.slice(0, ended);
|
|
509
|
+
if (script?.value === undefined ||
|
|
510
|
+
script.value.startsWith("-") ||
|
|
511
|
+
before.some((word) => word.value?.startsWith("-") !== false)) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
return { kind: "run", entry: undefined, preloads: [], runtime: undefined };
|
|
515
|
+
};
|
|
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;
|
|
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
|
+
};
|
|
544
|
+
/** `npm exec`, `npm x` and `npm run` with `run-script`. */
|
|
545
|
+
const npm = subcommands(new Map([
|
|
546
|
+
["exec", npmExec],
|
|
547
|
+
["x", npmExec],
|
|
548
|
+
["run", npmRun],
|
|
549
|
+
["run-script", npmRun],
|
|
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
|
+
};
|
|
592
|
+
/**
|
|
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.
|
|
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
|
+
};
|
|
605
|
+
const pnpm = subcommands(new Map([
|
|
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
|
+
};
|
|
614
|
+
/**
|
|
615
|
+
* `yarn node`, https://yarnpkg.com/cli/node, and `yarn dlx [-p <name>]... <command>`,
|
|
616
|
+
* https://yarnpkg.com/cli/dlx, whose command runs in the current directory.
|
|
617
|
+
*/
|
|
618
|
+
const yarn = subcommands(new Map([
|
|
619
|
+
[
|
|
620
|
+
"node",
|
|
621
|
+
(words) => ({ kind: "command", words: [{ value: "node", text: "node" }, ...words], at: -1 }),
|
|
622
|
+
],
|
|
623
|
+
[
|
|
624
|
+
"dlx",
|
|
625
|
+
(words) => {
|
|
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")));
|
|
630
|
+
},
|
|
631
|
+
],
|
|
632
|
+
]), YARN);
|
|
633
|
+
/** `bunx [-p <name>] <command>[@version] [arguments]`, https://bun.com/docs/pm/bunx */
|
|
634
|
+
const bunx = (words) => {
|
|
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
|
+
]),
|
|
959
|
+
};
|
|
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 };
|
|
1045
|
+
}
|
|
1046
|
+
/** The row of a tool that names its configuration file by `options` anywhere before a `--`. */
|
|
1047
|
+
function configuredBy(...options) {
|
|
1048
|
+
return { configurations: { valued: new Set(options) } };
|
|
1049
|
+
}
|
|
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
|
+
],
|
|
1201
|
+
[
|
|
1202
|
+
"npx",
|
|
1203
|
+
{
|
|
1204
|
+
...launcher(npx),
|
|
1205
|
+
// `-c` takes a command line, which is read as a configuration file's name as well, so
|
|
1206
|
+
// every configuration `fixtures/commands/observed.json` records stays named.
|
|
1207
|
+
configurations: {
|
|
1208
|
+
valued: new Set(["-c"]),
|
|
1209
|
+
beforeOperand: new Set(["--package", "-p", "--call"]),
|
|
1210
|
+
},
|
|
1211
|
+
},
|
|
1212
|
+
],
|
|
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 } }],
|
|
1235
|
+
// https://eslint.org/docs/latest/use/command-line-interface
|
|
1236
|
+
["eslint", configuredBy("-c", "--config")],
|
|
1237
|
+
// https://prettier.io/docs/cli, whose `-c` is `--check`.
|
|
1238
|
+
["prettier", configuredBy("--config")],
|
|
1239
|
+
// https://stylelint.io/user-guide/cli
|
|
1240
|
+
["stylelint", configuredBy("-c", "--config")],
|
|
1241
|
+
// https://jestjs.io/docs/cli
|
|
1242
|
+
["jest", configuredBy("-c", "--config")],
|
|
1243
|
+
// https://vitest.dev/guide/cli
|
|
1244
|
+
["vitest", configuredBy("-c", "--config")],
|
|
1245
|
+
// https://vite.dev/guide/cli
|
|
1246
|
+
["vite", configuredBy("-c", "--config")],
|
|
1247
|
+
// https://webpack.js.org/api/cli/
|
|
1248
|
+
["webpack", configuredBy("-c", "--config")],
|
|
1249
|
+
// https://mochajs.org/#command-line-usage, whose `-c` is `--color`.
|
|
1250
|
+
["mocha", configuredBy("--config")],
|
|
1251
|
+
]);
|
|
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));
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
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.
|
|
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. */
|
|
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) {
|
|
1546
|
+
const [program, ...rest] = words;
|
|
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 };
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* The configuration files one simple command names, as written: each static value of an option
|
|
1743
|
+
* its program's row names a configuration file by, and none for a program with no such row.
|
|
1744
|
+
*/
|
|
1745
|
+
export function configurationsOf(words) {
|
|
1746
|
+
const [program, ...rest] = words;
|
|
1747
|
+
const options = rowOf(program)?.configurations;
|
|
1748
|
+
const found = [];
|
|
1749
|
+
for (let at = 0; options !== undefined && at < rest.length; at += 1) {
|
|
1750
|
+
const word = rest[at]?.value;
|
|
1751
|
+
if (word === "--" || (word === undefined && options.beforeOperand !== undefined)) {
|
|
1752
|
+
break;
|
|
1753
|
+
}
|
|
1754
|
+
const equals = word?.startsWith("--") === true ? word.indexOf("=") : -1;
|
|
1755
|
+
const name = equals < 0 ? word : word?.slice(0, equals);
|
|
1756
|
+
if (name !== undefined && options.valued.has(name)) {
|
|
1757
|
+
const value = equals < 0 ? rest[(at += 1)]?.value : word?.slice(equals + 1);
|
|
1758
|
+
found.push(...(value === undefined || value === "" ? [] : [value]));
|
|
1759
|
+
}
|
|
1760
|
+
else if (options.beforeOperand !== undefined && name !== undefined) {
|
|
1761
|
+
if (!name.startsWith("-")) {
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
at += options.beforeOperand.has(name) && equals < 0 ? 1 : 0;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return found;
|
|
1768
|
+
}
|
|
1769
|
+
/** Either status, in a directory the analysis does not know. */
|
|
1770
|
+
const ELSEWHERE = [
|
|
1771
|
+
{ dir: undefined, ok: true },
|
|
1772
|
+
{ dir: undefined, ok: false },
|
|
1773
|
+
];
|
|
1774
|
+
const elsewhere = () => ELSEWHERE;
|
|
1775
|
+
/** The directory a builtin's one operand states, where it is static and starts with no character of `options`. */
|
|
1776
|
+
function statedDirectory(operands, options) {
|
|
1777
|
+
const [dir] = operands;
|
|
1778
|
+
return operands.length === 1 && dir?.value !== undefined && !options.includes(dir.value.charAt(0))
|
|
1779
|
+
? dir.value
|
|
1780
|
+
: undefined;
|
|
1781
|
+
}
|
|
1782
|
+
/** `cd` to a stated directory is taken to enter it, as `roots.13` states. */
|
|
1783
|
+
const cd = (operands, dir) => {
|
|
1784
|
+
const to = statedDirectory(operands, "-");
|
|
1785
|
+
return to === undefined
|
|
1786
|
+
? ELSEWHERE
|
|
1787
|
+
: [{ dir: dir === undefined ? undefined : resolvePath(dir, to), ok: true }];
|
|
1788
|
+
};
|
|
1789
|
+
/** `pushd` enters a stated directory, or fails and leaves the shell where it was. */
|
|
1790
|
+
const pushd = (operands, dir) => {
|
|
1791
|
+
const to = statedDirectory(operands, "-+");
|
|
1792
|
+
return to === undefined
|
|
1793
|
+
? ELSEWHERE
|
|
1794
|
+
: [
|
|
1795
|
+
{ dir: dir === undefined ? undefined : resolvePath(dir, to), ok: true },
|
|
1796
|
+
{ dir, ok: false },
|
|
1797
|
+
];
|
|
1798
|
+
};
|
|
1799
|
+
/**
|
|
1800
|
+
* `exec` with a command replaces the shell, which exits where it cannot; with none it only
|
|
1801
|
+
* redirects: https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#exec
|
|
1802
|
+
*/
|
|
1803
|
+
const exec = ([command]) => {
|
|
1804
|
+
if (command === undefined) {
|
|
1805
|
+
return undefined;
|
|
1806
|
+
}
|
|
1807
|
+
return command.value === undefined || command.value.startsWith("-") ? ELSEWHERE : [];
|
|
1808
|
+
};
|
|
1809
|
+
/** `exit` ends the shell whatever its operands, in dash and in bash. */
|
|
1810
|
+
const exit = () => [];
|
|
1811
|
+
/** The builtins that run another command in the same shell, where that one may be any builtin. */
|
|
1812
|
+
const runsBuiltin = (operands) => operands.some((word) => word.value === undefined || BUILTINS.has(word.value))
|
|
1813
|
+
? ELSEWHERE
|
|
1814
|
+
: undefined;
|
|
1815
|
+
/**
|
|
1816
|
+
* The shell builtins that may move or end the shell running them: `cd`,
|
|
1817
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/cd.html; `pushd` and `popd`,
|
|
1818
|
+
* https://www.gnu.org/software/bash/manual/html_node/Directory-Stack-Builtins.html; `exit`,
|
|
1819
|
+
* `exec`, and `return`, whose effect outside a function POSIX leaves unspecified,
|
|
1820
|
+
* https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#return; and `.`,
|
|
1821
|
+
* `eval`, `source`, `command` and `builtin`, which run other text or another builtin.
|
|
1822
|
+
*/
|
|
1823
|
+
const BUILTINS = new Map([
|
|
1824
|
+
["cd", cd],
|
|
1825
|
+
["pushd", pushd],
|
|
1826
|
+
["popd", elsewhere],
|
|
1827
|
+
["exit", exit],
|
|
1828
|
+
["exec", exec],
|
|
1829
|
+
["return", elsewhere],
|
|
1830
|
+
[".", elsewhere],
|
|
1831
|
+
["source", elsewhere],
|
|
1832
|
+
["eval", elsewhere],
|
|
1833
|
+
["command", runsBuiltin],
|
|
1834
|
+
["builtin", runsBuiltin],
|
|
1835
|
+
]);
|
|
1836
|
+
/**
|
|
1837
|
+
* Each way the shell running one simple command in its own environment, in `dir`, goes on
|
|
1838
|
+
* after a builtin that may move or end it, or `undefined` where the command is no such
|
|
1839
|
+
* builtin. A program word the shell computes may be any builtin.
|
|
1840
|
+
*/
|
|
1841
|
+
export function readBuiltin(words, dir) {
|
|
1842
|
+
const [program, ...rest] = words;
|
|
1843
|
+
if (program === undefined) {
|
|
1844
|
+
return undefined;
|
|
1845
|
+
}
|
|
1846
|
+
return program.value === undefined ? ELSEWHERE : BUILTINS.get(program.value)?.(rest, dir);
|
|
1847
|
+
}
|