@sema-agent/core 7.45.0 → 7.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +298 -1
  2. package/dist/agents/extra-tool-names.d.ts +65 -0
  3. package/dist/agents/extra-tool-names.js +124 -0
  4. package/dist/agents/subagent.d.ts +17 -5
  5. package/dist/agents/subagent.js +33 -25
  6. package/dist/agents/tool-filter.d.ts +17 -1
  7. package/dist/agents/tool-filter.js +7 -0
  8. package/dist/core/agent-config-dir.d.ts +20 -0
  9. package/dist/core/agent-config-dir.js +49 -0
  10. package/dist/core/allow-entry.d.ts +6 -4
  11. package/dist/core/allow-entry.js +6 -5
  12. package/dist/core/ask-origin.d.ts +35 -12
  13. package/dist/core/ask-origin.js +24 -7
  14. package/dist/core/checkpoint-store.d.ts +39 -2
  15. package/dist/core/effective-path-target.d.ts +136 -4
  16. package/dist/core/effective-path-target.js +110 -2
  17. package/dist/core/engine-notice.d.ts +12 -4
  18. package/dist/core/gate-fold.js +5 -2
  19. package/dist/core/gate-lanes.js +11 -4
  20. package/dist/core/gate-outcome.d.ts +4 -2
  21. package/dist/core/gate-outcome.js +2 -2
  22. package/dist/core/governance-codes.d.ts +3 -1
  23. package/dist/core/governance-codes.js +3 -0
  24. package/dist/core/hooks.d.ts +3 -0
  25. package/dist/core/path-rule-binding.d.ts +65 -0
  26. package/dist/core/path-rule-binding.js +99 -0
  27. package/dist/core/permission-rule-model.d.ts +59 -24
  28. package/dist/core/permission-rule-model.js +48 -44
  29. package/dist/core/permission-rule-provider.d.ts +7 -0
  30. package/dist/core/permission-rule-provider.js +10 -3
  31. package/dist/core/permission-rule-store.d.ts +8 -0
  32. package/dist/core/permission-rule-store.js +6 -2
  33. package/dist/core/permission-rule-sync.js +2 -2
  34. package/dist/core/permission-rules.d.ts +26 -45
  35. package/dist/core/permission-rules.js +77 -57
  36. package/dist/core/runner/child-tree.d.ts +97 -0
  37. package/dist/core/runner/child-tree.js +104 -0
  38. package/dist/core/runner/construction-doors.d.ts +5 -0
  39. package/dist/core/runner/construction-doors.js +6 -0
  40. package/dist/core/runner/contracts.d.ts +46 -0
  41. package/dist/core/runner/gate-exit.d.ts +4 -1
  42. package/dist/core/runner/gate-exit.js +5 -1
  43. package/dist/core/runner/permission-rule-lanes.d.ts +34 -25
  44. package/dist/core/runner/permission-rule-lanes.js +18 -12
  45. package/dist/core/runner/persisted-path-rows.d.ts +31 -7
  46. package/dist/core/runner/persisted-path-rows.js +31 -12
  47. package/dist/core/runner/prepare-ask-lane.d.ts +4 -0
  48. package/dist/core/runner/prepare-ask-lane.js +4 -2
  49. package/dist/core/runner/prepare-execution-env.d.ts +5 -2
  50. package/dist/core/runner/prepare-execution-env.js +10 -3
  51. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  52. package/dist/core/runner/prepare-gate-stations.js +2 -1
  53. package/dist/core/runner/prepare-hands-readface.d.ts +8 -4
  54. package/dist/core/runner/prepare-hands-readface.js +11 -11
  55. package/dist/core/runner/prepare-inherited-gate.d.ts +8 -1
  56. package/dist/core/runner/prepare-inherited-gate.js +8 -2
  57. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  58. package/dist/core/runner/prepare-park-ask.js +5 -3
  59. package/dist/core/runner/prepare-policy-chain.d.ts +14 -2
  60. package/dist/core/runner/prepare-policy-chain.js +13 -5
  61. package/dist/core/runner/prepare-suspend-saga.d.ts +10 -0
  62. package/dist/core/runner/prepare-suspend-saga.js +7 -1
  63. package/dist/core/runner/prepare-task.js +37 -18
  64. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -0
  65. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  66. package/dist/core/runner/prepare-workspace-restore.d.ts +3 -0
  67. package/dist/core/runner/prepare-workspace-restore.js +1 -0
  68. package/dist/core/runner/resume-apply.js +17 -3
  69. package/dist/core/runner/resume-checkpoint-screen.js +13 -0
  70. package/dist/core/runner/run-leg.js +5 -1
  71. package/dist/core/runner/runtask.js +2 -2
  72. package/dist/core/runner/sub-agent-deny-lists.d.ts +1 -1
  73. package/dist/core/runner/write-protection-station.d.ts +3 -2
  74. package/dist/core/runner/write-protection-station.js +1 -0
  75. package/dist/core/runner-deps.d.ts +14 -0
  76. package/dist/core/sensitive-path-policy.d.ts +8 -0
  77. package/dist/core/sensitive-path-policy.js +18 -2
  78. package/dist/core/shell-wrapper-projection.d.ts +11 -0
  79. package/dist/core/shell-wrapper-projection.js +56 -0
  80. package/dist/core/shell-wrapper-table.d.ts +144 -34
  81. package/dist/core/shell-wrapper-table.js +40 -20
  82. package/dist/core/synthetic-read-verdict.d.ts +3 -0
  83. package/dist/core/synthetic-read-verdict.js +3 -1
  84. package/dist/core/tool-catalog-entries.d.ts +21 -0
  85. package/dist/core/tool-catalog-entries.js +12 -2
  86. package/dist/core/tool-face.d.ts +10 -1
  87. package/dist/core/tool-name-readings.d.ts +8 -0
  88. package/dist/core/tool-name-readings.js +2 -0
  89. package/dist/core/tool-policy.d.ts +40 -17
  90. package/dist/core/tool-policy.js +11 -9
  91. package/dist/core/tool-registry.d.ts +9 -0
  92. package/dist/core/tool-registry.js +10 -0
  93. package/dist/core/tool-spec.d.ts +3 -2
  94. package/dist/core/write-protect.d.ts +14 -3
  95. package/dist/core/write-protect.js +41 -10
  96. package/dist/index.d.ts +6 -4
  97. package/dist/index.js +5 -3
  98. package/dist/orchestration/run-spec.js +8 -7
  99. package/dist/orchestration/workflow.js +1 -1
  100. package/dist/shell/lex.d.ts +22 -0
  101. package/dist/shell/lex.js +75 -13
  102. package/dist/tools/fs/bash-program-position.d.ts +14 -90
  103. package/dist/tools/fs/bash-program-position.js +41 -74
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +17 -444
  105. package/dist/tools/fs/bash-readonly-classifier.js +169 -1225
  106. package/dist/tools/fs/index.d.ts +3 -2
  107. package/dist/tools/fs/program-read-model.d.ts +711 -0
  108. package/dist/tools/fs/program-read-model.js +1259 -0
  109. package/dist/tools/fs/program-read-model.rows.d.ts +18 -0
  110. package/dist/tools/fs/program-read-model.rows.js +624 -0
  111. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  112. package/dist/tools/fs/read-deny-home.js +62 -24
  113. package/dist/tools/fs/read-deny.d.ts +54 -14
  114. package/dist/tools/fs/read-deny.js +104 -50
  115. package/dist/tools/fs/removal-targets.d.ts +7 -6
  116. package/dist/tools/fs/removal-targets.js +34 -24
  117. package/dist/tools/fs/safety.d.ts +3 -0
  118. package/dist/tools/fs/safety.js +10 -3
  119. package/dist/tools/fs/shell-read-argv.d.ts +86 -0
  120. package/dist/tools/fs/shell-read-argv.js +388 -0
  121. package/dist/tools/fs/shell-read-bindings.d.ts +300 -0
  122. package/dist/tools/fs/shell-read-bindings.js +755 -0
  123. package/dist/tools/fs/shell-read-check.js +23 -15
  124. package/dist/tools/fs/shell-read-establish.js +82 -8
  125. package/dist/tools/fs/shell-read-judge.d.ts +11 -1
  126. package/dist/tools/fs/shell-read-judge.js +9 -1
  127. package/dist/tools/fs/shell-read-plan.d.ts +33 -8
  128. package/dist/tools/fs/shell-read-plan.js +309 -407
  129. package/dist/tools/fs/shell-read-word.d.ts +60 -26
  130. package/dist/tools/fs/shell-read-word.js +47 -37
  131. package/dist/tools/fs/shell-read-write-targets.d.ts +15 -64
  132. package/dist/tools/fs/shell-read-write-targets.js +85 -137
  133. package/dist/tools/fs/shell-reentry-text.d.ts +136 -6
  134. package/dist/tools/fs/shell-reentry-text.js +203 -53
  135. package/dist/tools/monitor.d.ts +3 -3
  136. package/package.json +1 -1
  137. package/test/export-surface.snapshot.json +25 -5
  138. package/wire-changes/core@7.46.0.json +1522 -0
  139. package/wire-changes/core@7.47.0.json +412 -0
@@ -1,6 +1,8 @@
1
1
  import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf } from "./safety.js";
2
2
  import { PARSE_FAILURE_SENTENCE, UNRESOLVED_SENTENCE, argvOf, lexShell, literalTildeSpelling, } from "../../shell/lex.js";
3
- import { COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
3
+ import { textBindsHome } from "./shell-read-bindings.js";
4
+ import { NOT_AUTO_ALLOWED, argvReentryUnjudged, isBareControlWord, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
5
+ import { BLOCK_WORD_NAMES, attachedStdinFile, declaresOption, firstOperandSlot, opensNoOperand, programDelivery, programRow, programTextReadings, readArgRoles, readArgs, readProgramTextLanguage, runsShellText, selectedOptions, slotValueCandidates, suppliesFirstOperandSlot, takesSeparatedDatum, valuesAreLiteral, writeReadings, writesPath, } from "./program-read-model.js";
4
6
  export const BASH_READONLY_DEFAULT_ALLOW = [
5
7
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
6
8
  "grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
@@ -121,7 +123,7 @@ export function strictResidualRefusal(lexed, source, opts = {}) {
121
123
  }
122
124
  }
123
125
  }
124
- const home = opts.homeDir !== undefined && !homeMayBeRebound(source) ? opts.homeDir : undefined;
126
+ const home = opts.homeDir !== undefined && !textBindsHome(source) ? opts.homeDir : undefined;
125
127
  for (const w of words) {
126
128
  if (expandsDollar(w) && !(home !== undefined && homeSpelledForClassify(w, home) !== undefined))
127
129
  return reason;
@@ -159,99 +161,6 @@ export function formatOutOfRootReadApprovalOption(directory) {
159
161
  const leaf = cut >= 0 ? trimmed.slice(cut + 1) : trimmed;
160
162
  return `Yes, allow reading from ${leaf || directory}/ from this project`;
161
163
  }
162
- export const NO_PATH_OPERAND_COMMANDS = new Set([
163
- "pwd", "echo", "printf", "whoami", "uname", "which", "tr", "basename", "dirname",
164
- "cal", "uptime", "id", "groups", "nproc", "locale", "free", "true", "false", "seq", "expr", "type", "sleep",
165
- "test", "[",
166
- ]);
167
- export const NO_PATH_OPERAND_EVALUATORS = new Set(["test", "["]);
168
- export const DECLARATION_BUILTINS = new Set(["export", "declare", "typeset", "local", "readonly"]);
169
- export const INERT_BUILTINS = new Set(["echo", "true", "false", "pwd", ...DECLARATION_BUILTINS]);
170
- const MESSAGE_SLOTS = {
171
- git: {
172
- globalPlain: ["-p", "--paginate", "-P", "--no-pager", "--bare", "--no-replace-objects", "--literal-pathspecs", "--glob-pathspecs", "--noglob-pathspecs", "--icase-pathspecs", "--no-optional-locks"],
173
- globalValue: ["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--config-env", "--super-prefix"],
174
- subcommands: {
175
- commit: { plain: "apsnvqeioz", optional: "Su", value: "mFCct", longValue: ["message", "file", "template", "pathspec-from-file", "author", "date", "cleanup", "fixup", "squash", "trailer", "reuse-message", "reedit-message"] },
176
- tag: { plain: "asfdvle", optional: "n", value: "mFu", longValue: ["message", "file", "local-user", "cleanup", "trailer", "sort", "format", "contains", "no-contains", "points-at", "merged", "no-merged"] },
177
- merge: { plain: "nqve", optional: "S", value: "mFsX", longValue: ["message", "file", "strategy", "strategy-option", "into-name", "cleanup"] },
178
- stash: { plain: "pkuaqS", optional: "", value: "m", longValue: ["message", "pathspec-from-file"] },
179
- },
180
- },
181
- };
182
- export function messageSlotWords(program, words, programIndex) {
183
- const out = new Set();
184
- const row = MESSAGE_SLOTS[program];
185
- if (row === undefined)
186
- return out;
187
- let j = programIndex + 1;
188
- for (; j < words.length; j++) {
189
- const w = words[j].folded;
190
- if (!w.startsWith("-"))
191
- break;
192
- if (row.globalValue.includes(w))
193
- j++;
194
- else if (!row.globalPlain.includes(w) && !row.globalValue.some((o) => o.startsWith("--") && w.startsWith(`${o}=`)))
195
- return out;
196
- }
197
- const sub = row.subcommands[words[j]?.folded ?? ""];
198
- if (sub === undefined)
199
- return out;
200
- const settled = (k) => words[k] !== undefined && words[k].expands === false;
201
- let pending;
202
- for (let k = j + 1; k < words.length; k++) {
203
- const w = words[k].folded;
204
- if (pending !== undefined) {
205
- if (pending === "message" && settled(k))
206
- out.add(k);
207
- pending = undefined;
208
- continue;
209
- }
210
- if (w === "--")
211
- break;
212
- if (!settled(k)) {
213
- pending = "other";
214
- continue;
215
- }
216
- if (!w.startsWith("-") || w === "-")
217
- continue;
218
- if (w.startsWith("--")) {
219
- const eq = w.indexOf("=");
220
- const name = w.slice(2, eq < 0 ? undefined : eq);
221
- const takers = sub.longValue.filter((n) => n.startsWith(name));
222
- const message = name.length > 0 && takers.length > 0 && takers.every((n) => n === "message") || name === "message";
223
- if (eq >= 0) {
224
- if (message)
225
- out.add(k);
226
- continue;
227
- }
228
- if (takers.length > 0)
229
- pending = message ? "message" : "other";
230
- continue;
231
- }
232
- const letters = w.slice(1);
233
- for (let i = 0; i < letters.length; i++) {
234
- const c = letters[i];
235
- if (sub.plain.includes(c))
236
- continue;
237
- if (sub.optional.includes(c))
238
- break;
239
- if (sub.value.includes(c)) {
240
- if (i < letters.length - 1) {
241
- if (c === "m")
242
- out.add(k);
243
- }
244
- else
245
- pending = c === "m" ? "message" : "other";
246
- break;
247
- }
248
- pending = "other";
249
- break;
250
- }
251
- }
252
- return out;
253
- }
254
- const XARGS_TARGETS_ACCEPTED_UNJUDGED = NO_PATH_OPERAND_COMMANDS;
255
164
  export function isPathShapedToken(tok) {
256
165
  return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
257
166
  }
@@ -287,7 +196,6 @@ function spellingsOf(absolute) {
287
196
  function reheadSegment(toks, head, from = 0) {
288
197
  return { folded: [head, ...toks.folded.slice(from + 1)], raw: [head, ...toks.raw.slice(from + 1)] };
289
198
  }
290
- export const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
291
199
  function segmentSpellings(words, homeDir, homeRebound) {
292
200
  const folded = [];
293
201
  const raw = [];
@@ -303,6 +211,8 @@ export function expansionInWord(w) {
303
211
  const k = p.expansion?.kind;
304
212
  if (k === "parameter" || k === "command" || k === "process" || k === "arithmetic" || k === "brace")
305
213
  return true;
214
+ if (k === "glob" && /[$`]/.test(p.expansion?.body ?? ""))
215
+ return true;
306
216
  if (p.literal === undefined)
307
217
  continue;
308
218
  if (p.quotedBy === undefined && /[{}$`]/.test(p.literal))
@@ -322,437 +232,6 @@ export function globInWord(w) {
322
232
  }
323
233
  return false;
324
234
  }
325
- function longOptionNameOf(tok) {
326
- if (!tok.startsWith("--") || tok.length === 2)
327
- return undefined;
328
- const body = tok.slice(2);
329
- const eq = body.indexOf("=");
330
- const name = eq >= 0 ? body.slice(0, eq) : body;
331
- return name.length > 0 ? name : undefined;
332
- }
333
- function isLongOptionAbbrevOf(name, full) {
334
- return full.startsWith(name);
335
- }
336
- export function suppliesFirstOperandSlot(slot, tok) {
337
- if (tok.startsWith("--")) {
338
- const long = longOptionNameOf(tok);
339
- return long !== undefined && slot.suppliedByLongNames.some((full) => isLongOptionAbbrevOf(long, full));
340
- }
341
- return new RegExp(`^-[A-Za-z0-9]*[${slot.suppliedByLetters}]`).test(tok);
342
- }
343
- function isGrepClusterFileStdin(tok) {
344
- if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
345
- return false;
346
- for (let i = 1; i < tok.length; i++) {
347
- const ch = tok[i];
348
- if (ch === "e")
349
- return false;
350
- if (ch === "f")
351
- return tok.slice(i + 1) === "-";
352
- if (!/[A-Za-z0-9]/.test(ch))
353
- return false;
354
- }
355
- return false;
356
- }
357
- function valueNamesAFile(kind) {
358
- return kind === "path" || kind === "indirect" || kind === "form" || kind === "header" || kind === "url";
359
- }
360
- function widestValueKind(kinds) {
361
- if (kinds.has("indirect"))
362
- return "indirect";
363
- if (kinds.has("path"))
364
- return "path";
365
- if (kinds.has("form"))
366
- return "form";
367
- if (kinds.has("header"))
368
- return "header";
369
- if (kinds.has("url"))
370
- return "url";
371
- return [...kinds][0];
372
- }
373
- const VERB_OPTION_SLOTS = {
374
- head: {
375
- shortValue: { c: "count", n: "count" },
376
- shortPlain: "qvz",
377
- longValue: { bytes: "count", lines: "count" },
378
- longOptionalValue: {},
379
- longPlain: ["help", "quiet", "silent", "verbose", "version", "zero-terminated"],
380
- },
381
- tail: {
382
- shortValue: { b: "count", c: "count", n: "count", s: "count" },
383
- shortPlain: "fFqvz",
384
- longValue: { bytes: "count", lines: "count", "max-unchanged-stats": "count", pid: "count", "sleep-interval": "count" },
385
- longOptionalValue: { follow: "label" },
386
- longPlain: ["help", "quiet", "retry", "silent", "verbose", "version", "zero-terminated"],
387
- },
388
- cut: {
389
- shortValue: { b: "count", c: "count", d: "delimiter", f: "count" },
390
- shortPlain: "nsz",
391
- longValue: { bytes: "count", characters: "count", delimiter: "delimiter", fields: "count", "output-delimiter": "delimiter" },
392
- longOptionalValue: {},
393
- longPlain: ["complement", "help", "only-delimited", "version", "zero-terminated"],
394
- },
395
- grep: {
396
- shortValue: { A: "count", B: "count", C: "count", D: "label", d: "label", e: "pattern", f: "path", m: "count" },
397
- shortPlain: "0123456789abcEFGhHiIlLnoPqrRsUvVwxyzZ",
398
- longValue: {
399
- "after-context": "count",
400
- "before-context": "count",
401
- "binary-files": "label",
402
- devices: "label",
403
- directories: "label",
404
- exclude: "name-pattern",
405
- "exclude-dir": "name-pattern",
406
- "exclude-from": "path",
407
- file: "path",
408
- "group-separator": "label",
409
- include: "name-pattern",
410
- label: "label",
411
- "max-count": "count",
412
- regexp: "pattern",
413
- },
414
- longOptionalValue: { color: "label", colour: "label", context: "count" },
415
- firstOperand: { kind: "pattern", suppliedByLetters: "ef", suppliedByLongNames: ["regexp", "file"] },
416
- longPlain: [
417
- "basic-regexp", "byte-offset", "count", "dereference-recursive", "extended-regexp",
418
- "files-with-matches", "files-without-match", "fixed-strings", "help", "ignore-case", "initial-tab",
419
- "invert-match", "line-buffered", "line-number", "line-regexp", "no-filename", "no-group-separator",
420
- "no-messages", "null", "null-data", "only-matching", "perl-regexp", "quiet", "recursive", "silent",
421
- "text", "version", "with-filename", "word-regexp",
422
- ],
423
- },
424
- diff: {
425
- shortValue: { C: "count", D: "label", F: "pattern", I: "pattern", L: "label", S: "path", U: "count", W: "count", X: "path", x: "name-pattern" },
426
- shortPlain: "abBcdeEHilnNpqrstTuvwyZ",
427
- longValue: {
428
- exclude: "name-pattern",
429
- "exclude-from": "path",
430
- "from-file": "path",
431
- "horizon-lines": "count",
432
- ifdef: "label",
433
- "ignore-matching-lines": "pattern",
434
- label: "label",
435
- "show-function-line": "pattern",
436
- "starting-file": "path",
437
- tabsize: "count",
438
- "to-file": "path",
439
- width: "count",
440
- },
441
- longOptionalValue: { color: "label", context: "count", unified: "count" },
442
- longPlain: [
443
- "brief", "ed", "expand-tabs", "help", "ignore-all-space", "ignore-blank-lines", "ignore-case",
444
- "ignore-space-change", "ignore-tab-expansion", "ignore-trailing-space", "initial-tab", "minimal",
445
- "new-file", "normal", "paginate", "recursive", "report-identical-files", "rcs", "show-c-function",
446
- "side-by-side", "speed-large-files", "suppress-blank-empty", "text", "version",
447
- ],
448
- },
449
- cmp: {
450
- shortValue: { i: "count", n: "count" },
451
- shortPlain: "blsv",
452
- longValue: { bytes: "count", "ignore-initial": "count" },
453
- longOptionalValue: {},
454
- longPlain: ["help", "print-bytes", "quiet", "silent", "verbose", "version"],
455
- },
456
- sed: {
457
- shortValue: { e: "script", f: "path" },
458
- shortPlain: "nrEszu",
459
- longValue: { expression: "script", file: "path", "line-length": "count" },
460
- longOptionalValue: { "in-place": "label" },
461
- firstOperand: { kind: "script", suppliedByLetters: "ef", suppliedByLongNames: ["expression", "file"] },
462
- longPlain: [
463
- "debug", "follow-symlinks", "help", "null-data", "posix", "quiet", "regexp-extended", "sandbox",
464
- "separate", "silent", "unbuffered", "version", "zero-terminated",
465
- ],
466
- },
467
- date: {
468
- shortValue: { d: "count", f: "path", r: "path", s: "count" },
469
- shortPlain: "uR",
470
- longValue: { date: "count", file: "path", reference: "path", "rfc-3339": "count", set: "count" },
471
- longOptionalValue: { "iso-8601": "label" },
472
- longPlain: ["debug", "help", "rfc-822", "rfc-email", "universal", "utc", "version"],
473
- },
474
- tar: {
475
- shortValue: { b: "count", C: "path", f: "path", T: "path", X: "path" },
476
- shortPlain: "acdhjJkmOpPrStuvwWxzZ",
477
- longValue: {
478
- "blocking-factor": "count",
479
- directory: "path",
480
- exclude: "name-pattern",
481
- "exclude-from": "path",
482
- file: "path",
483
- "files-from": "path",
484
- "strip-components": "count",
485
- },
486
- longOptionalValue: {},
487
- longPlain: [
488
- "absolute-names", "append", "create", "dereference", "extract", "get", "help", "interactive",
489
- "keep-old-files", "list", "preserve-permissions", "sparse", "to-stdout", "touch", "update",
490
- "verbose", "verify", "version",
491
- ],
492
- },
493
- find: {
494
- shortValue: { D: "label", O: "count", f: "path" },
495
- shortPlain: "HLPdsx",
496
- longValue: {},
497
- longOptionalValue: {},
498
- longPlain: ["help", "version"],
499
- dashWordValue: {
500
- amin: "count", anewer: "path", atime: "count", cmin: "count", cnewer: "path", context: "label",
501
- ctime: "count", fstype: "label", gid: "count", group: "label", ilname: "path", iname: "name-pattern",
502
- inum: "count", ipath: "path", iregex: "name-pattern", iwholename: "path", links: "count",
503
- lname: "path", maxdepth: "count", mindepth: "count", mmin: "count", mtime: "count", name: "name-pattern",
504
- newer: "path", newerat: "path", neweraa: "path", neweram: "path", newerac: "path",
505
- newerct: "path", newerca: "path", newercm: "path", newercc: "path",
506
- newermt: "path", newerma: "path", newermm: "path", newermc: "path",
507
- newerbt: "path", newerba: "path", newerbm: "path", newerbc: "path",
508
- path: "path", perm: "label", printf: "label", regex: "name-pattern", regextype: "label",
509
- samefile: "path", size: "count", type: "label", uid: "count", used: "count", user: "label",
510
- wholename: "path", xtype: "label",
511
- },
512
- dashWordPlain: [
513
- "and", "daystart", "depth", "empty", "executable", "false", "follow", "help", "ignore_readdir_race",
514
- "mount", "noignore_readdir_race", "noleaf", "not", "nogroup", "nouser", "or", "print",
515
- "print0", "prune", "quit", "readable", "true", "version", "writable", "xdev",
516
- ],
517
- optionsArePositionIndependent: true,
518
- },
519
- curl: {
520
- shortValue: {
521
- A: "label", b: "indirect", C: "count", c: "path", D: "path", d: "indirect", E: "path", e: "label",
522
- F: "form", H: "header", K: "path", m: "count", o: "path", P: "label", Q: "label", r: "count",
523
- T: "path", t: "label", U: "label", u: "label", w: "indirect", X: "label", x: "label", Y: "count",
524
- y: "count", z: "path",
525
- },
526
- shortPlain: "#0123456aBfgGhiIjJklLMnNOpqRsSvVZ",
527
- longValue: {
528
- cacert: "path",
529
- capath: "path",
530
- cert: "path",
531
- config: "path",
532
- "connect-timeout": "count",
533
- cookie: "indirect",
534
- "cookie-jar": "path",
535
- data: "indirect",
536
- "data-ascii": "indirect",
537
- "data-binary": "indirect",
538
- "data-raw": "label",
539
- "data-urlencode": "form",
540
- "dump-header": "path",
541
- form: "form",
542
- "form-string": "label",
543
- header: "header",
544
- key: "path",
545
- "max-time": "count",
546
- "netrc-file": "path",
547
- output: "path",
548
- proxy: "label",
549
- range: "count",
550
- referer: "label",
551
- request: "label",
552
- resolve: "label",
553
- trace: "path",
554
- "time-cond": "path",
555
- "trace-ascii": "path",
556
- "upload-file": "path",
557
- url: "url",
558
- user: "label",
559
- "user-agent": "label",
560
- "write-out": "indirect",
561
- },
562
- longOptionalValue: {},
563
- longPlain: [
564
- "append", "compressed", "disable", "fail", "get", "globoff", "head", "help", "include", "insecure",
565
- "junk-session-cookies", "list-only", "location", "manual", "netrc", "next", "no-buffer", "parallel",
566
- "proxytunnel", "remote-header-name", "remote-name", "remote-time", "show-error", "silent",
567
- "use-ascii", "verbose", "version",
568
- ],
569
- },
570
- };
571
- export function readVerbOptionSlot(name, tok) {
572
- const row = VERB_OPTION_SLOTS[name];
573
- if (row === undefined)
574
- return undefined;
575
- if (!tok.startsWith("-") || tok === "-" || tok === "--")
576
- return { form: "unexplained" };
577
- if (tok.startsWith("--")) {
578
- const long = longOptionNameOf(tok);
579
- if (long === undefined)
580
- return { form: "unexplained" };
581
- const eq = tok.indexOf("=");
582
- const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
583
- const withValue = (kind) => attached !== undefined ? { form: "attached", value: attached, kind } : { form: "separated", kind };
584
- const exact = row.longValue[long];
585
- if (exact !== undefined)
586
- return withValue(exact);
587
- const exactOptional = row.longOptionalValue[long];
588
- if (exactOptional !== undefined)
589
- return attached !== undefined ? { form: "attached", value: attached, kind: exactOptional } : { form: "no-value" };
590
- if (row.longPlain.includes(long))
591
- return attached === undefined ? { form: "no-value" } : { form: "unexplained" };
592
- const valueNames = [
593
- ...Object.keys(row.longValue),
594
- ...(attached !== undefined ? Object.keys(row.longOptionalValue) : []),
595
- ].filter((full) => isLongOptionAbbrevOf(long, full));
596
- const plainNames = [
597
- ...row.longPlain,
598
- ...(attached === undefined ? Object.keys(row.longOptionalValue) : []),
599
- ].filter((full) => isLongOptionAbbrevOf(long, full));
600
- if (valueNames.length === 0)
601
- return plainNames.length > 0 && attached === undefined ? { form: "no-value" } : { form: "unexplained" };
602
- const kinds = new Set(valueNames.map((full) => (row.longValue[full] ?? row.longOptionalValue[full])));
603
- if (attached !== undefined)
604
- return { form: "attached", value: attached, kind: widestValueKind(kinds) };
605
- if (plainNames.length > 0 || kinds.size !== 1)
606
- return { form: "unexplained" };
607
- return withValue([...kinds][0]);
608
- }
609
- const word = row.dashWordValue !== undefined || row.dashWordPlain !== undefined ? /^-([A-Za-z_][\w-]+)$/.exec(tok)?.[1] : undefined;
610
- if (word !== undefined) {
611
- if (row.shortValue[word[0]] !== undefined)
612
- return { form: "unexplained" };
613
- const kind = row.dashWordValue?.[word];
614
- if (kind !== undefined)
615
- return { form: "separated", kind };
616
- if (row.dashWordPlain?.includes(word) === true)
617
- return { form: "no-value" };
618
- return { form: "unexplained" };
619
- }
620
- for (let i = 1; i < tok.length; i++) {
621
- const ch = tok[i];
622
- const kind = row.shortValue[ch];
623
- if (kind !== undefined) {
624
- const rest = tok.slice(i + 1);
625
- return rest !== "" ? { form: "attached", value: rest, kind } : { form: "separated", kind };
626
- }
627
- if (!row.shortPlain.includes(ch))
628
- return { form: "unexplained" };
629
- }
630
- return { form: "no-value" };
631
- }
632
- export function verbOptionsArePositionIndependent(name) {
633
- return VERB_OPTION_SLOTS[name]?.optionsArePositionIndependent === true;
634
- }
635
- export function verbFirstOperandSlot(name) {
636
- return VERB_OPTION_SLOTS[name]?.firstOperand;
637
- }
638
- function localFileUrlPath(value) {
639
- const m = /^file:(?:\/\/[^/?#]*)?(\/[^?#]*)/i.exec(value.trim());
640
- if (m === null)
641
- return undefined;
642
- const raw = m[1];
643
- try {
644
- return decodeURIComponent(raw);
645
- }
646
- catch {
647
- return raw;
648
- }
649
- }
650
- function formSpecFileNames(value) {
651
- const out = [];
652
- const after = (s) => {
653
- const at = s.search(/[@<]/);
654
- if (at !== -1 && at < s.length - 1)
655
- out.push(s.slice(at + 1));
656
- };
657
- after(value);
658
- for (const part of value.split(/[;,]/))
659
- after(part);
660
- return [...new Set(out)];
661
- }
662
- export function slotValueCandidates(value, kind) {
663
- if (!valueNamesAFile(kind))
664
- return [];
665
- if (kind === "url") {
666
- const local = localFileUrlPath(value);
667
- return local !== undefined ? [local] : [];
668
- }
669
- if (kind === "form")
670
- return formSpecFileNames(value).map(literalTildeSpelling);
671
- const named = value.startsWith("@") && value.length > 1 ? literalTildeSpelling(value.slice(1)) : undefined;
672
- if (kind === "header")
673
- return named !== undefined ? [value, named] : [];
674
- const out = [value];
675
- if (kind === "indirect" && named !== undefined)
676
- out.push(named);
677
- return out;
678
- }
679
- const SEPARATED_VALUE_OPTIONS = {
680
- head: { short: "cn", long: ["bytes", "lines"] },
681
- tail: { short: "cns", long: ["bytes", "lines", "sleep-interval", "pid", "max-unchanged-stats"] },
682
- cut: { short: "bcdf", long: ["bytes", "characters", "delimiter", "fields", "output-delimiter"] },
683
- grep: {
684
- ownsValueButNotSkippable: "ef",
685
- short: "mABCdD",
686
- long: [
687
- "max-count",
688
- "after-context",
689
- "before-context",
690
- "context",
691
- "devices",
692
- "directories",
693
- "label",
694
- "binary-files",
695
- "group-separator",
696
- "include",
697
- "exclude",
698
- "exclude-dir",
699
- ],
700
- },
701
- cmp: { short: "ni", long: ["bytes", "ignore-initial"] },
702
- diff: { short: "UCWIFxL", ownsValueButNotSkippable: "SX", long: ["unified", "context", "width", "ignore-matching-lines", "show-function-line", "exclude", "label", "horizon-lines", "tabsize"] },
703
- };
704
- function takesSeparatedValue(name, tok) {
705
- const model = SEPARATED_VALUE_OPTIONS[name];
706
- if (model === undefined)
707
- return false;
708
- if (tok.startsWith("--")) {
709
- if (tok.includes("="))
710
- return false;
711
- const long = longOptionNameOf(tok);
712
- return long !== undefined && model.long.some((full) => isLongOptionAbbrevOf(long, full));
713
- }
714
- if (!/^-[A-Za-z0-9]/.test(tok))
715
- return false;
716
- for (let i = 1; i < tok.length; i++) {
717
- const ch = tok[i];
718
- if ((model.ownsValueButNotSkippable ?? "").includes(ch))
719
- return false;
720
- if (model.short.includes(ch))
721
- return i === tok.length - 1;
722
- if (!/[A-Za-z0-9]/.test(ch))
723
- return false;
724
- }
725
- return false;
726
- }
727
- const GREP_RECURSIVE_READ_FORM = {
728
- discloses: "contents",
729
- shortLetters: "rR",
730
- valueOwners: "efmABCD",
731
- longNames: ["recursive", "dereference-recursive"],
732
- enumOptions: [{ shortLetter: "d", longName: "directories", recursiveValue: "recurse" }],
733
- dashIsStdin: true,
734
- };
735
- export const RECURSIVE_READ_FORMS = {
736
- grep: GREP_RECURSIVE_READ_FORM,
737
- egrep: GREP_RECURSIVE_READ_FORM,
738
- fgrep: GREP_RECURSIVE_READ_FORM,
739
- ls: { discloses: "names", shortLetters: "R", longNames: ["recursive"] },
740
- du: { discloses: "names", always: true },
741
- find: { discloses: "names", always: true },
742
- rg: { discloses: "contents", always: true, dashIsStdin: true },
743
- tree: { discloses: "names", always: true },
744
- ag: { discloses: "contents", always: true, dashIsStdin: true },
745
- ack: { discloses: "contents", always: true, dashIsStdin: true },
746
- tar: { discloses: "contents", shortLetters: "cru", valueOwners: "fCTXbg", longNames: ["create", "append", "update"], bundledModeLetters: "cru", dashIsStdin: true },
747
- diff: { discloses: "contents", shortLetters: "r", valueOwners: "UCWISFXx", longNames: ["recursive"], dashIsStdin: true },
748
- };
749
- const FIND_DANGEROUS_PREDICATES = new Set([
750
- "-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fls", "-fprintf", "-files0-from",
751
- ]);
752
- const SED_READONLY_FLAGS = new Set([
753
- "-n", "--quiet", "--silent", "-E", "--regexp-extended", "-r", "-z", "--zero-terminated", "--posix",
754
- ]);
755
- const SED_READONLY_CLUSTER_LETTERS = new Set(["n", "E", "r", "z"]);
756
235
  const SED_READONLY_SCRIPT_PIECE = /^(?:\d+|\d+,\d+)?p$/;
757
236
  const SED_FILE_OR_EXEC_COMMANDS = new Set(["r", "R", "w", "W", "e"]);
758
237
  const SED_PLAIN_COMMANDS = new Set(["=", "d", "D", "F", "g", "G", "h", "H", "n", "N", "p", "P", "x", "z"]);
@@ -989,103 +468,18 @@ function readSedScript(script) {
989
468
  return failed();
990
469
  return anyName() ? named() : "clean";
991
470
  }
992
- const SED_OPTION_DIALECTS = [
993
- { separated: "efl", suffix: "iI", stopsAtFirstOperand: false },
994
- { separated: "ef", suffix: "iI", stopsAtFirstOperand: true },
995
- ];
996
- function sedSeparatedSuffixTaken(tok) {
997
- return tok !== undefined && (tok === "" || tok.startsWith("."));
998
- }
999
- const SED_LONG_VALUE_OPTIONS = ["expression", "line-length"];
1000
- const SED_LONG_PLAIN_OPTIONS = [
1001
- "debug", "follow-symlinks", "help", "in-place", "null-data", "posix", "quiet", "regexp-extended",
1002
- "sandbox", "separate", "silent", "unbuffered", "version", "zero-terminated",
1003
- ];
1004
- function readSedInvocation(args, dialect) {
1005
- return sedReadingKind(readSedInvocationDetailed(args, dialect));
1006
- }
1007
- function readSedInvocationDetailed(args, dialect) {
1008
- const scripts = [];
1009
- let scriptSupplied = false;
1010
- let endOfOptions = false;
1011
- let firstOperand;
1012
- for (let k = 0; k < args.length; k++) {
1013
- const tok = args[k];
1014
- if (!endOfOptions && tok === "--") {
1015
- endOfOptions = true;
1016
- continue;
1017
- }
1018
- if (endOfOptions || !tok.startsWith("-") || tok === "-") {
1019
- if (firstOperand === undefined) {
1020
- firstOperand = tok;
1021
- if (dialect.stopsAtFirstOperand)
1022
- endOfOptions = true;
1023
- }
1024
- continue;
1025
- }
1026
- if (tok.startsWith("--")) {
1027
- const long = longOptionNameOf(tok);
1028
- if (long === undefined)
1029
- return "unparsed";
1030
- const eq = tok.indexOf("=");
1031
- const attached = eq >= 0 ? tok.slice(eq + 1) : undefined;
1032
- if (isLongOptionAbbrevOf(long, "file"))
1033
- return "unseen";
1034
- if (isLongOptionAbbrevOf(long, "expression")) {
1035
- const value = attached ?? args[++k];
1036
- if (value === undefined)
1037
- return "unparsed";
1038
- scripts.push(value);
1039
- scriptSupplied = true;
1040
- continue;
1041
- }
1042
- if (SED_LONG_VALUE_OPTIONS.some((full) => isLongOptionAbbrevOf(long, full))) {
1043
- if (attached === undefined)
1044
- k++;
1045
- continue;
1046
- }
1047
- if (SED_LONG_PLAIN_OPTIONS.some((full) => isLongOptionAbbrevOf(long, full)))
1048
- continue;
1049
- return "unparsed";
1050
- }
1051
- for (let c = 1; c < tok.length; c++) {
1052
- const letter = tok[c];
1053
- if (dialect.suffix.includes(letter)) {
1054
- if (c + 1 === tok.length && sedSeparatedSuffixTaken(args[k + 1]))
1055
- k++;
1056
- break;
1057
- }
1058
- if (dialect.separated.includes(letter)) {
1059
- const rest = tok.slice(c + 1);
1060
- const value = rest !== "" ? rest : args[++k];
1061
- if (letter === "f")
1062
- return "unseen";
1063
- if (letter === "e") {
1064
- if (value === undefined)
1065
- return "unparsed";
1066
- scripts.push(value);
1067
- scriptSupplied = true;
1068
- }
1069
- break;
1070
- }
1071
- if (!SED_SHORT_PLAIN_LETTERS.includes(letter))
1072
- return "unparsed";
1073
- }
1074
- }
1075
- const script = scriptSupplied ? scripts.join("\n") : firstOperand;
1076
- if (script === undefined)
1077
- return "clean";
1078
- return readSedScript(script);
471
+ function scriptReadings(row, args) {
472
+ return programTextReadings(row, args).map((r) => (r === "from-file" ? "unseen" : r === "unparsed" ? "unparsed" : r === "none" ? "clean" : readSedScript(r.text)));
1079
473
  }
1080
- const SED_SHORT_PLAIN_LETTERS = "nsuzEraHl";
1081
- export function sedScriptSlotFiles(args) {
474
+ export function programTextSlotFiles(row, args) {
475
+ if (readProgramTextLanguage(row) !== "sed")
476
+ return undefined;
1082
477
  const reads = new Set();
1083
478
  const writes = new Set();
1084
479
  const commands = new Set();
1085
480
  let executesPatternSpace = false;
1086
481
  const kinds = [];
1087
- for (const dialect of SED_OPTION_DIALECTS) {
1088
- const reading = readSedInvocationDetailed(args, dialect);
482
+ for (const reading of scriptReadings(row, args)) {
1089
483
  kinds.push(sedReadingKind(reading));
1090
484
  if (typeof reading === "string")
1091
485
  continue;
@@ -1104,335 +498,111 @@ export function sedScriptSlotFiles(args) {
1104
498
  : undefined;
1105
499
  return { reads: [...reads], writes: [...writes], commands: [...commands], executesPatternSpace, ...(unread !== undefined ? { unread } : {}) };
1106
500
  }
1107
- const SCRIPT_SLOT_VERBS = new Map([
1108
- ["sed", (args) => {
1109
- const readings = SED_OPTION_DIALECTS.map((dialect) => readSedInvocation(args, dialect));
1110
- if (readings.includes("names") || readings.includes("unseen") || readings.includes("oversized"))
1111
- return true;
1112
- return readings.every((reading) => reading === "unparsed");
1113
- }],
1114
- ]);
501
+ function scriptSlotHidesNames(row, args) {
502
+ if (readProgramTextLanguage(row) !== "sed")
503
+ return false;
504
+ const readings = scriptReadings(row, args).map(sedReadingKind);
505
+ if (readings.includes("names") || readings.includes("unseen") || readings.includes("oversized"))
506
+ return true;
507
+ return readings.every((reading) => reading === "unparsed");
508
+ }
1115
509
  const INLINE_PROGRAM_TEXT = "runs its argument as an interpreted program";
1116
510
  const INLINE_PROGRAM_STDIN = "reads the program it runs from its standard input";
1117
- const INTERPRETER_BANNER_OPTIONS = new Set(["--version", "--help", "--usage", "-version", "-help"]);
1118
- const BLANK_INLINE = {
1119
- longText: new Set(), longFile: new Set(), longNamesProgram: new Set(), longSupplemental: new Set(),
1120
- longValue: new Set(), longPlain: new Set(), fileOptionEndsOptions: false,
1121
- textLetters: "", fileLetters: "", separatedValueLetters: "", attachedValueLetters: "",
1122
- plainLetters: "", bannerLetters: "", interactiveLetters: "",
1123
- programOperand: { at: 0, isText: false },
1124
- };
1125
- const PERL_INLINE = {
1126
- ...BLANK_INLINE, textLetters: "eEmM", separatedValueLetters: "I", attachedValueLetters: "iCFxDV",
1127
- plainLetters: "nplawcsStTUWX", bannerLetters: "vVh", stdinWhenUnnamed: true,
1128
- };
1129
- const RUBY_INLINE = {
1130
- ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "IrECFKT", plainLetters: "nplawcsSU",
1131
- bannerLetters: "vh", stdinWhenUnnamed: true,
1132
- };
1133
- const PYTHON_INLINE = {
1134
- ...BLANK_INLINE, textLetters: "c", fileLetters: "m", separatedValueLetters: "WXQ",
1135
- plainLetters: "uBEIsSqbdvxOtR", bannerLetters: "Vh?", interactiveLetters: "i",
1136
- fileOptionEndsOptions: true, stdinWhenUnnamed: true,
1137
- };
1138
- const NODE_INLINE = {
1139
- ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
1140
- longValue: new Set(["--import", "--loader", "--experimental-loader", "--require", "--env-file", "--conditions", "--max-old-space-size"]),
1141
- longNamesProgram: new Set(["--test"]),
1142
- longPlain: new Set(["--inspect", "--inspect-brk", "--no-warnings", "--trace-warnings", "--watch", "--enable-source-maps", "--experimental-modules", "--no-deprecation", "--check"]),
1143
- separatedValueLetters: "r", plainLetters: "c", bannerLetters: "vh", interactiveLetters: "i",
1144
- stdinWhenUnnamed: true,
1145
- };
1146
- const PHP_INLINE = {
1147
- ...BLANK_INLINE, longText: new Set(["--run", "--process-begin", "--process-code", "--process-end"]),
1148
- textLetters: "rRBE", fileLetters: "fF", separatedValueLetters: "dcz", plainLetters: "nqswl",
1149
- bannerLetters: "vhi", interactiveLetters: "a", stdinWhenUnnamed: true,
1150
- };
1151
- const LUA_INLINE = {
1152
- ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "l", bannerLetters: "vh",
1153
- interactiveLetters: "i", stdinWhenUnnamed: true,
1154
- };
1155
- const TCL_INLINE = { ...BLANK_INLINE, stdinWhenUnnamed: true };
1156
- const EXPECT_INLINE = {
1157
- ...BLANK_INLINE, textLetters: "c", separatedValueLetters: "Db", plainLetters: "dN",
1158
- bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: true,
1159
- };
1160
- const AWK_INLINE = {
1161
- ...BLANK_INLINE, longText: new Set(["--source"]), textLetters: "e",
1162
- longFile: new Set(["--file", "--exec"]), fileLetters: "fE", separatedValueLetters: "vF",
1163
- bannerLetters: "V", programOperand: { at: 0, isText: true }, stdinWhenUnnamed: false,
1164
- };
1165
- const JQ_INLINE = {
1166
- ...BLANK_INLINE, longFile: new Set(["--from-file"]), fileLetters: "f", separatedValueLetters: "L",
1167
- plainLetters: "nrjcsSaCMRe", bannerLetters: "h", programOperand: { at: 0, isText: true },
1168
- stdinWhenUnnamed: false,
1169
- };
1170
- const BC_INLINE = {
1171
- ...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e", plainLetters: "lqsw",
1172
- bannerLetters: "vh", interactiveLetters: "i", programOperand: undefined, stdinWhenUnnamed: true,
1173
- };
1174
- const DC_INLINE = {
1175
- ...BLANK_INLINE, longText: new Set(["--expression"]), textLetters: "e",
1176
- longFile: new Set(["--file"]), fileLetters: "f", bannerLetters: "hV",
1177
- programOperand: undefined, stdinWhenUnnamed: true,
1178
- };
1179
- const SQLITE_INLINE = {
1180
- ...BLANK_INLINE, longText: new Set(["-cmd"]), longSupplemental: new Set(["-init"]),
1181
- programOperand: { at: 1, isText: true }, stdinWhenUnnamed: true,
1182
- };
1183
- const PSQL_INLINE = {
1184
- ...BLANK_INLINE, longText: new Set(["--command"]), textLetters: "c",
1185
- longFile: new Set(["--file"]), fileLetters: "f", separatedValueLetters: "hpUdoF",
1186
- plainLetters: "aeEqtxXwW", bannerLetters: "lV?", programOperand: undefined, stdinWhenUnnamed: true,
1187
- };
1188
- const OSASCRIPT_INLINE = {
1189
- ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "ls", plainLetters: "i", bannerLetters: "h",
1190
- stdinWhenUnnamed: true,
1191
- };
1192
- const RSCRIPT_INLINE = {
1193
- ...BLANK_INLINE, textLetters: "e", bannerLetters: "h", stdinWhenUnnamed: false,
1194
- };
1195
- const JULIA_INLINE = {
1196
- ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "eE",
1197
- separatedValueLetters: "JLp", plainLetters: "q", bannerLetters: "vh", interactiveLetters: "i",
1198
- stdinWhenUnnamed: true,
1199
- };
1200
- const GHCI_INLINE = {
1201
- ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "io", programOperand: undefined,
1202
- stdinWhenUnnamed: true,
1203
- };
1204
- const GROOVY_INLINE = {
1205
- ...BLANK_INLINE, textLetters: "e", separatedValueLetters: "Dcl", plainLetters: "n",
1206
- bannerLetters: "vh", interactiveLetters: "i", stdinWhenUnnamed: false,
1207
- };
1208
- const BUN_INLINE = {
1209
- ...BLANK_INLINE, longText: new Set(["--eval", "--print"]), textLetters: "ep",
1210
- separatedValueLetters: "r", bannerLetters: "vh", stdinWhenUnnamed: false,
1211
- };
1212
- export const INTERPRETER_INLINE_PROGRAMS = new Map([
1213
- ["perl", PERL_INLINE],
1214
- ["ruby", RUBY_INLINE],
1215
- ["python", PYTHON_INLINE],
1216
- ["node", NODE_INLINE],
1217
- ["nodejs", NODE_INLINE],
1218
- ["php", PHP_INLINE],
1219
- ["lua", LUA_INLINE],
1220
- ["luajit", LUA_INLINE],
1221
- ["tclsh", TCL_INLINE],
1222
- ["wish", TCL_INLINE],
1223
- ["expect", EXPECT_INLINE],
1224
- ["awk", AWK_INLINE],
1225
- ["gawk", AWK_INLINE],
1226
- ["mawk", AWK_INLINE],
1227
- ["nawk", AWK_INLINE],
1228
- ["jq", JQ_INLINE],
1229
- ["bc", BC_INLINE],
1230
- ["dc", DC_INLINE],
1231
- ["sqlite", SQLITE_INLINE],
1232
- ["psql", PSQL_INLINE],
1233
- ["osascript", OSASCRIPT_INLINE],
1234
- ["Rscript", RSCRIPT_INLINE],
1235
- ["julia", JULIA_INLINE],
1236
- ["ghci", GHCI_INLINE],
1237
- ["groovy", GROOVY_INLINE],
1238
- ["bun", BUN_INLINE],
1239
- ]);
1240
- export function interpreterKey(name) {
1241
- return name.replace(/\d+(?:\.\d+)*$/, "");
511
+ function unjudgedProgramTextRefusal(written, clause) {
512
+ return `command "${written}" ${clause}, which this walk does not judge ${NOT_AUTO_ALLOWED}`;
513
+ }
514
+ function argumentAsProgramRefusal(written, name, args) {
515
+ const row = programRow(name);
516
+ if (row !== undefined && scriptSlotHidesNames(row, args)) {
517
+ return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
518
+ }
519
+ const delivery = programDelivery(row, args);
520
+ return delivery === undefined ? undefined : unjudgedProgramTextRefusal(written, delivery === "text" ? INLINE_PROGRAM_TEXT : INLINE_PROGRAM_STDIN);
521
+ }
522
+ function makesMoreThanARead(spec) {
523
+ return spec.effect === "writes" || spec.effect === "executes" || spec.delivers !== undefined || spec.selects === "edit-in-place" || writesPath(spec.value);
1242
524
  }
1243
- export function inlineProgramClause(model, args) {
525
+ function writesWordsItIsHanded(row, args) {
526
+ if (row === undefined)
527
+ return false;
528
+ const operands = args.some((w) => !w.startsWith("-") || w === "-");
529
+ return writeReadings(row, args).some((r) => r.written.length > 0 || (r.unexplained && r.operandRole !== undefined && operands));
530
+ }
531
+ function inReadOnlyForm(row, flag) {
532
+ const plain = (spec) => spec !== undefined && spec.arity === 0 && spec.effect === "none" && spec.fillsSlot !== true;
533
+ const merged = (table, key) => {
534
+ const specs = row.grammars.map((g) => g[table][key]).filter((x) => x !== undefined);
535
+ return specs.length > 0 && specs.every(plain) ? specs[0] : undefined;
536
+ };
537
+ if (flag.startsWith("--"))
538
+ return !flag.includes("=") && merged("long", flag.slice(2)) !== undefined;
539
+ return /^-[A-Za-z]+$/.test(flag) && [...flag.slice(1)].every((ch) => merged("short", ch) !== undefined);
540
+ }
541
+ function selectsQuiet(row, flag) {
542
+ const quiet = (spec) => spec?.mode === "quiet";
543
+ if (flag.startsWith("--"))
544
+ return row.grammars.some((g) => quiet(g.long[flag.slice(2)]));
545
+ return /^-[A-Za-z]+$/.test(flag) && [...flag.slice(1)].some((ch) => row.grammars.some((g) => quiet(g.short[ch])));
546
+ }
547
+ function readOnlyFormReason(name, row, rest, args) {
548
+ for (const w of args) {
549
+ if (globInWord(w) || expansionInWord(w)) {
550
+ return `\`${name}\` is given the unquoted pattern "${w.raw}", which the shell may expand into other arguments (a file named like a flag becomes a live option) before ${name} runs ${NOT_AUTO_ALLOWED}`;
551
+ }
552
+ }
553
+ const flags = [];
554
+ const operands = [];
1244
555
  let endOfOptions = false;
1245
- let banner = false;
1246
- let interactive = false;
1247
- let named = false;
1248
- let stdin = false;
1249
- let explained = true;
1250
- let operands = 0;
1251
- scan: for (let i = 0; i < args.length; i++) {
1252
- const arg = args[i];
1253
- if (!endOfOptions && arg === "--") {
556
+ for (const t of rest) {
557
+ if (!endOfOptions && t === "--") {
1254
558
  endOfOptions = true;
1255
559
  continue;
1256
560
  }
1257
- if (arg === "-") {
1258
- if (model.programOperand?.isText !== true)
1259
- stdin = true;
1260
- continue;
1261
- }
1262
- if (!endOfOptions && arg.startsWith("-")) {
1263
- const eq = arg.indexOf("=");
1264
- const whole = eq < 0 ? arg : arg.slice(0, eq);
1265
- const valueOf = () => (eq < 0 ? args[++i] : arg.slice(eq + 1));
1266
- if (model.longText.has(whole))
1267
- return INLINE_PROGRAM_TEXT;
1268
- if (model.longFile.has(whole)) {
1269
- if (valueOf() === "-") {
1270
- stdin = true;
1271
- continue;
1272
- }
1273
- named = true;
1274
- if (explained && model.fileOptionEndsOptions)
1275
- break scan;
1276
- continue;
1277
- }
1278
- if (model.longNamesProgram.has(whole)) {
1279
- named = true;
1280
- continue;
1281
- }
1282
- if (model.longSupplemental.has(whole)) {
1283
- valueOf();
1284
- continue;
1285
- }
1286
- if (INTERPRETER_BANNER_OPTIONS.has(whole)) {
1287
- banner = true;
1288
- continue;
1289
- }
1290
- if (model.longValue.has(whole)) {
1291
- valueOf();
1292
- continue;
1293
- }
1294
- if (model.longPlain.has(whole))
1295
- continue;
1296
- if (arg.startsWith("--")) {
1297
- if (eq < 0)
1298
- explained = false;
1299
- continue;
1300
- }
1301
- for (let k = 1; k < arg.length; k++) {
1302
- const ch = arg[k];
1303
- if (ch >= "0" && ch <= "9")
1304
- continue;
1305
- if (model.textLetters.includes(ch))
1306
- return INLINE_PROGRAM_TEXT;
1307
- if (model.fileLetters.includes(ch)) {
1308
- const value = k + 1 < arg.length ? arg.slice(k + 1) : args[++i];
1309
- if (value === "-") {
1310
- stdin = true;
1311
- break;
1312
- }
1313
- named = true;
1314
- if (explained && model.fileOptionEndsOptions)
1315
- break scan;
1316
- break;
1317
- }
1318
- if (model.attachedValueLetters.includes(ch)) {
1319
- if (model.bannerLetters.includes(ch))
1320
- banner = true;
1321
- break;
1322
- }
1323
- if (model.bannerLetters.includes(ch)) {
1324
- banner = true;
1325
- continue;
1326
- }
1327
- if (model.interactiveLetters.includes(ch)) {
1328
- interactive = true;
1329
- continue;
1330
- }
1331
- if (model.separatedValueLetters.includes(ch)) {
1332
- if (k + 1 >= arg.length)
1333
- i++;
1334
- break;
1335
- }
1336
- if (!model.plainLetters.includes(ch))
1337
- explained = false;
1338
- }
561
+ if (!endOfOptions && t.startsWith("-")) {
562
+ flags.push(t);
1339
563
  continue;
1340
564
  }
1341
- operands++;
1342
- if (!named && explained && model.programOperand !== undefined && operands === model.programOperand.at + 1) {
1343
- if (model.programOperand.isText)
1344
- return INLINE_PROGRAM_TEXT;
1345
- named = true;
1346
- break;
1347
- }
565
+ operands.push(t);
1348
566
  }
1349
- if (stdin || interactive)
1350
- return INLINE_PROGRAM_STDIN;
1351
- if (named || banner)
1352
- return undefined;
1353
- return model.stdinWhenUnnamed ? INLINE_PROGRAM_STDIN : undefined;
1354
- }
1355
- function unjudgedProgramTextRefusal(written, clause) {
1356
- return `command "${written}" ${clause}, which this walk does not judge ${NOT_AUTO_ALLOWED}`;
1357
- }
1358
- function argumentAsProgramRefusal(written, name, args) {
1359
- const slot = SCRIPT_SLOT_VERBS.get(name);
1360
- if (slot !== undefined && slot(args)) {
1361
- return unjudgedProgramTextRefusal(written, "names files or runs commands inside its script text");
567
+ for (const f of flags)
568
+ if (!inReadOnlyForm(row, f))
569
+ return `\`${name} ${f}\` is outside the read-only ${name} form (-n plus print-only script) ${NOT_AUTO_ALLOWED}`;
570
+ if (!flags.some((f) => selectsQuiet(row, f)))
571
+ return `\`${name}\` without \`-n\` echoes its whole input — only the -n print-only form is read-classified ${NOT_AUTO_ALLOWED}`;
572
+ const script = operands[0];
573
+ if (script === undefined)
574
+ return `\`${name}\` with no script ${NOT_AUTO_ALLOWED}`;
575
+ for (const piece of script.split(";")) {
576
+ if (!SED_READONLY_SCRIPT_PIECE.test(piece.trim()))
577
+ return `\`${name}\` script "${script}" is not a pure print command (N[,M]p) ${NOT_AUTO_ALLOWED}`;
1362
578
  }
1363
- const interpreter = INTERPRETER_INLINE_PROGRAMS.get(interpreterKey(name));
1364
- if (interpreter === undefined)
1365
- return undefined;
1366
- const clause = inlineProgramClause(interpreter, args);
1367
- return clause === undefined ? undefined : unjudgedProgramTextRefusal(written, clause);
579
+ return undefined;
1368
580
  }
1369
581
  function guardedVerbFormReason(name, toks, args) {
1370
- if (name === "find") {
1371
- for (const t of toks) {
1372
- if (FIND_DANGEROUS_PREDICATES.has(t)) {
1373
- return `\`find ${t}\` executes a command or writes a file — not a read ${NOT_AUTO_ALLOWED}`;
1374
- }
582
+ const row = programRow(name);
583
+ const rest = toks.slice(1);
584
+ if (row !== undefined && readProgramTextLanguage(row) !== undefined)
585
+ return readOnlyFormReason(name, row, rest, args);
586
+ if (row !== undefined && declaresOption(row, makesMoreThanARead)) {
587
+ for (const t of rest) {
588
+ if (selectedOptions(row, [t]).some(makesMoreThanARead))
589
+ return `\`${name} ${t}\` executes a command or writes a file — not a read ${NOT_AUTO_ALLOWED}`;
1375
590
  }
1376
591
  for (const w of args) {
1377
592
  if (globInWord(w) || expansionInWord(w)) {
1378
- return `\`find\` is given the unquoted pattern "${w.raw}", which the shell may expand into other arguments before find runs ${NOT_AUTO_ALLOWED}`;
1379
- }
1380
- }
1381
- return undefined;
1382
- }
1383
- if (name === "sed") {
1384
- for (const w of args) {
1385
- if (globInWord(w) || expansionInWord(w)) {
1386
- return `\`sed\` is given the unquoted pattern "${w.raw}", which the shell may expand into other arguments (a file named like a flag becomes a live option) before sed runs ${NOT_AUTO_ALLOWED}`;
1387
- }
1388
- }
1389
- const flags = [];
1390
- const operands = [];
1391
- let endOfOptions = false;
1392
- for (const t of toks.slice(1)) {
1393
- if (!endOfOptions && t === "--") {
1394
- endOfOptions = true;
1395
- continue;
1396
- }
1397
- if (!endOfOptions && t.startsWith("-")) {
1398
- flags.push(t);
1399
- continue;
1400
- }
1401
- operands.push(t);
1402
- }
1403
- for (const f of flags) {
1404
- if (SED_READONLY_FLAGS.has(f))
1405
- continue;
1406
- if (/^-[A-Za-z]+$/.test(f) && f.length > 2 && [...f.slice(1)].every((ch) => SED_READONLY_CLUSTER_LETTERS.has(ch)))
1407
- continue;
1408
- return `\`sed ${f}\` is outside the read-only sed form (-n plus print-only script) ${NOT_AUTO_ALLOWED}`;
1409
- }
1410
- const quiet = flags.some((f) => f === "-n" || f === "--quiet" || f === "--silent" || (/^-[A-Za-z]+$/.test(f) && f.includes("n")));
1411
- if (!quiet)
1412
- return `\`sed\` without \`-n\` echoes its whole input — only the -n print-only form is read-classified ${NOT_AUTO_ALLOWED}`;
1413
- const script = operands[0];
1414
- if (script === undefined)
1415
- return `\`sed\` with no script ${NOT_AUTO_ALLOWED}`;
1416
- for (const piece of script.split(";")) {
1417
- if (!SED_READONLY_SCRIPT_PIECE.test(piece.trim())) {
1418
- return `\`sed\` script "${script}" is not a pure print command (N[,M]p) ${NOT_AUTO_ALLOWED}`;
1419
- }
1420
- }
1421
- return undefined;
1422
- }
1423
- if (name === "free") {
1424
- for (const t of toks.slice(1)) {
1425
- if (/^--seconds(=.*)?$/.test(t) || /^-[A-Za-z0-9]*s[A-Za-z0-9]*$/.test(t)) {
1426
- return `\`free ${t}\` repeats until killed — it never terminates on its own ${NOT_AUTO_ALLOWED}`;
593
+ return `\`${name}\` is given the unquoted pattern "${w.raw}", which the shell may expand into other arguments before ${name} runs ${NOT_AUTO_ALLOWED}`;
1427
594
  }
1428
595
  }
1429
596
  return undefined;
1430
597
  }
598
+ const repeats = rest.find((t) => selectedOptions(row, [t]).some((s) => s.mode === "repeats"));
599
+ if (repeats !== undefined)
600
+ return `\`${name} ${repeats}\` repeats until killed — it never terminates on its own ${NOT_AUTO_ALLOWED}`;
1431
601
  if (name === "sleep") {
1432
602
  return pollLoopSleepReason(toks);
1433
603
  }
1434
604
  if (name === "seq") {
1435
- for (const t of toks.slice(1)) {
605
+ for (const t of rest) {
1436
606
  if (t.startsWith("-") && t !== "-" && !/^-\d/.test(t))
1437
607
  continue;
1438
608
  if (!/^-?\d{1,6}$/.test(t)) {
@@ -1441,92 +611,38 @@ function guardedVerbFormReason(name, toks, args) {
1441
611
  }
1442
612
  return undefined;
1443
613
  }
1444
- if (name === "diff") {
1445
- for (const t of toks.slice(1)) {
1446
- if (/^--[A-Za-z-]+=-$/.test(t) || /^-[A-Za-z]*[SX]-$/.test(t)) {
1447
- return `\`diff ${t}\` names stdin inside a fused option — a read this check cannot see through ${NOT_AUTO_ALLOWED}`;
1448
- }
1449
- }
1450
- }
1451
614
  return undefined;
1452
615
  }
1453
- export function segmentSelectsRecursiveRead(name, args) {
1454
- const model = RECURSIVE_READ_FORMS[name];
1455
- if (model === undefined)
1456
- return false;
1457
- if (model.discloses !== "contents")
1458
- return false;
1459
- if (model.always === true)
1460
- return true;
1461
- if (model.bundledModeLetters !== undefined) {
1462
- const first = args.find((t) => t.length > 0);
1463
- if (first !== undefined && !first.startsWith("-") && /^[A-Za-z]+$/.test(first) && [...first].some((ch) => model.bundledModeLetters.includes(ch))) {
1464
- return true;
1465
- }
1466
- }
1467
- let endOfOptions = false;
1468
- for (let k = 0; k < args.length; k++) {
1469
- const t = args[k];
1470
- if (endOfOptions)
1471
- continue;
1472
- if (t === "--") {
1473
- const prev = k > 0 ? args[k - 1] : undefined;
1474
- const prevMayOwnValue = prev !== undefined && prev.startsWith("--") && prev.length > 2 && !prev.includes("=");
1475
- if (!prevMayOwnValue)
1476
- endOfOptions = true;
1477
- continue;
1478
- }
1479
- if (t.startsWith("--")) {
1480
- const long = longOptionNameOf(t);
1481
- if (long === undefined)
1482
- continue;
1483
- if (model.longNames?.some((full) => isLongOptionAbbrevOf(long, full)) === true)
1484
- return true;
1485
- for (const en of model.enumOptions ?? []) {
1486
- if (en.longName === undefined || !isLongOptionAbbrevOf(long, en.longName))
1487
- continue;
1488
- const eq = t.indexOf("=");
1489
- const v = eq >= 0 ? t.slice(eq + 1) : args[k + 1];
1490
- if (v !== undefined && v.length > 0 && en.recursiveValue.startsWith(v.toLowerCase()))
1491
- return true;
1492
- }
1493
- continue;
616
+ function openableWordsOf(row, args) {
617
+ const out = [];
618
+ const globs = valuesAreLiteral(row, args) ? "literal" : "expanded";
619
+ const offer = (value, role) => {
620
+ const named = slotValueCandidates(value, role, globs);
621
+ out.push(...(named ?? [value]));
622
+ };
623
+ for (const e of readArgs(row, args.map((folded) => ({ folded })), { firstOperandSlot: true })) {
624
+ const t = args[e.at];
625
+ if (e.kind === "dash-word-operand")
626
+ out.push(t);
627
+ else if (e.kind === "value")
628
+ offer(t, e.role);
629
+ else if (e.kind === "option") {
630
+ if (e.reading === "speculative")
631
+ out.push(...attachedOptionPayloads(t));
632
+ else if (e.reading !== "no-value")
633
+ offer(literalTildeSpelling(e.reading.attached), e.reading.role);
1494
634
  }
1495
- if (!t.startsWith("-") || t === "-")
1496
- continue;
1497
- for (let i = 1; i < t.length; i++) {
1498
- const ch = t[i];
1499
- if (model.shortLetters?.includes(ch) === true)
1500
- return true;
1501
- const en = (model.enumOptions ?? []).find((e) => e.shortLetter === ch);
1502
- if (en !== undefined) {
1503
- const v = i === t.length - 1 ? args[k + 1] : t.slice(i + 1);
1504
- if (v !== undefined && v.length > 0 && en.recursiveValue.startsWith(v.toLowerCase()))
1505
- return true;
1506
- if (i === t.length - 1)
1507
- k++;
1508
- break;
1509
- }
1510
- if (model.valueOwners?.includes(ch) === true) {
1511
- if (i === t.length - 1)
1512
- k++;
1513
- break;
1514
- }
1515
- if (!/[A-Za-z0-9]/.test(ch))
1516
- break;
635
+ else if (e.kind === "operand") {
636
+ const role = e.slot ?? e.role;
637
+ if (role !== undefined)
638
+ offer(t, role);
639
+ else
640
+ out.push(t);
1517
641
  }
1518
642
  }
1519
- return false;
1520
- }
1521
- function isGrepFileStdinLongOption(tok) {
1522
- const eq = tok.indexOf("=");
1523
- if (eq < 0 || tok.slice(eq + 1) !== "-")
1524
- return false;
1525
- const name = longOptionNameOf(tok);
1526
- return name !== undefined && isLongOptionAbbrevOf(name, "file");
643
+ return out;
1527
644
  }
1528
645
  const CLUSTER_OPTION_CHAR = /[A-Za-z0-9]/;
1529
- export const ASSIGNMENT_VALUE_NOT_PATHS = new Set(["make", "gmake", "bmake", "cmake", "ninja", "meson", "docker", "podman"]);
1530
646
  export function argvProgramOf(words) {
1531
647
  const pos = readProgramPosition(words);
1532
648
  if (pos.program === undefined)
@@ -1580,26 +696,6 @@ const CLASSIFY_HOME_REREADABLE = /["$`\\\n\r]/;
1580
696
  function homeSpelledForClassify(w, home) {
1581
697
  return CLASSIFY_HOME_REREADABLE.test(home) ? undefined : homeExpansionOf(w, home)?.spelled;
1582
698
  }
1583
- const HOME_PLAIN_READ = /\$HOME(?![A-Za-z0-9_])|\$\{HOME\}/g;
1584
- const HOME_NAME = /HOME(?![A-Za-z0-9_])/;
1585
- export function homeMayBeRebound(text) {
1586
- const unquoted = text.replace(/\\\r?\n/g, "").replace(HOME_PLAIN_READ, " ").replace(/\$?["']|\\/g, "");
1587
- if (HOME_NAME.test(unquoted))
1588
- return true;
1589
- if (hasUnquotedExtglobOpener(text))
1590
- return true;
1591
- if (!unquoted.includes("printf") && !/[*?[{]/.test(unquoted))
1592
- return false;
1593
- const lexed = lexShell(text);
1594
- return !("parseFailure" in lexed) && lexed.segments.some((seg) => {
1595
- const words = argvOf(seg);
1596
- return printfNamesComputed(words) || words.some((w) => globMayMatchHome(w) || optionWordCarriesGlob(w) || wordHasBraceList(w));
1597
- });
1598
- }
1599
- function hasUnquotedExtglobOpener(text) {
1600
- const lexed = lexShell(text);
1601
- return "parseFailure" in lexed || extglobOpenerIn(lexed, text);
1602
- }
1603
699
  export function extglobOpenerIn(lexed, text) {
1604
700
  for (const seg of lexed.segments) {
1605
701
  for (const [k, tok] of seg.tokens.entries()) {
@@ -1623,147 +719,6 @@ export function extglobOpenerIn(lexed, text) {
1623
719
  }
1624
720
  return false;
1625
721
  }
1626
- function wordHasBraceList(w) {
1627
- return w.pieces.some((p) => p.expansion?.kind === "brace");
1628
- }
1629
- function optionWordCarriesGlob(w) {
1630
- if (!w.pieces.some(isGlobPiece))
1631
- return false;
1632
- const atoms = patternAtoms(w.pieces);
1633
- if (atoms === undefined)
1634
- return UNREADABLE_GLOB_IS_A_REBINDING;
1635
- try {
1636
- const reversed = atoms.slice().reverse();
1637
- return closureReaches(atoms, "-") && (closureReaches(reversed, "EMOH") || closureReaches(reversed, "]"));
1638
- }
1639
- catch {
1640
- return UNREADABLE_GLOB_IS_A_REBINDING;
1641
- }
1642
- }
1643
- const UNREADABLE_GLOB_IS_A_REBINDING = true;
1644
- function escapeRegExp(s) {
1645
- return s.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
1646
- }
1647
- function globMayMatchHome(w) {
1648
- if (!w.pieces.some(isGlobPiece))
1649
- return false;
1650
- const atoms = patternAtoms(w.pieces);
1651
- if (atoms === undefined)
1652
- return UNREADABLE_GLOB_IS_A_REBINDING;
1653
- try {
1654
- if (new RegExp(`^${atoms.join("")}$`).test("HOME"))
1655
- return true;
1656
- return closureReaches(atoms, "HOME[") && closureReaches(atoms.slice().reverse(), "]");
1657
- }
1658
- catch {
1659
- return UNREADABLE_GLOB_IS_A_REBINDING;
1660
- }
1661
- }
1662
- function patternAtoms(pieces) {
1663
- const atoms = [];
1664
- for (const p of pieces) {
1665
- if (p.literal !== undefined && p.expansion === undefined) {
1666
- for (const ch of p.literal)
1667
- atoms.push(caseFoldedChar(ch));
1668
- continue;
1669
- }
1670
- if (isGlobPiece(p)) {
1671
- const body = p.expansion.body;
1672
- if (body === undefined) {
1673
- atoms.push(RUN_ATOM);
1674
- continue;
1675
- }
1676
- const cls = translateBracket(body);
1677
- if (cls === undefined)
1678
- return undefined;
1679
- atoms.push(cls);
1680
- continue;
1681
- }
1682
- atoms.push(RUN_ATOM);
1683
- }
1684
- return atoms;
1685
- }
1686
- const RUN_ATOM = ".*";
1687
- function caseFoldedChar(ch) {
1688
- const lower = ch.toLowerCase();
1689
- const upper = ch.toUpperCase();
1690
- return lower !== upper ? `[${escapeRegExp(lower)}${escapeRegExp(upper)}]` : escapeRegExp(ch);
1691
- }
1692
- function closureReaches(atoms, target) {
1693
- let re = "";
1694
- for (let i = atoms.length - 1; i >= 0; i--)
1695
- re = `(?:${atoms[i]}${re})?`;
1696
- return new RegExp(`^${re}$`).test(target);
1697
- }
1698
- function scanBracketBody(body) {
1699
- const members = [];
1700
- let computed = false;
1701
- for (let i = 0; i < body.length; i++) {
1702
- const c = body[i];
1703
- if (c === "\\") {
1704
- if (i + 1 < body.length)
1705
- members.push({ ch: body[++i], unquoted: false });
1706
- continue;
1707
- }
1708
- if (c === "$" || c === "`" || c === "{")
1709
- computed = true;
1710
- members.push({ ch: c, unquoted: true });
1711
- }
1712
- return { members, computed };
1713
- }
1714
- function membersOpenClassExpression(members) {
1715
- return members.some((m, i) => m.unquoted && m.ch === "[" && members[i + 1]?.unquoted === true && (members[i + 1].ch === ":" || members[i + 1].ch === "=" || members[i + 1].ch === "."));
1716
- }
1717
- function translateBracket(body) {
1718
- if (body.includes('"') || body.includes("'"))
1719
- return undefined;
1720
- const { members, computed } = scanBracketBody(body);
1721
- if (computed || membersOpenClassExpression(members))
1722
- return undefined;
1723
- let i = 0;
1724
- let negated = false;
1725
- if (members[0]?.unquoted === true && (members[0].ch === "!" || members[0].ch === "^")) {
1726
- negated = true;
1727
- i = 1;
1728
- }
1729
- const first = i;
1730
- let cls = "";
1731
- for (; i < members.length; i++) {
1732
- const { ch, unquoted } = members[i];
1733
- if (ch === "-" && unquoted && i > first && i < members.length - 1) {
1734
- if (!negated)
1735
- return undefined;
1736
- cls += "-";
1737
- continue;
1738
- }
1739
- const escaped = ch.replace(/[\\\]\[^-]/g, "\\$&");
1740
- cls += !negated && ch.toLowerCase() !== ch.toUpperCase() ? `${escapeRegExp(ch.toLowerCase())}${escapeRegExp(ch.toUpperCase())}` : escaped;
1741
- }
1742
- return `[${negated ? "^" : ""}${cls || "\\]"}]`;
1743
- }
1744
- const literalWord = (w) => (w !== undefined && w.pieces.every((p) => p.literal !== undefined && p.expansion === undefined) ? w.pieces.map((p) => p.literal).join("") : undefined);
1745
- const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1746
- function printfNamesComputed(words) {
1747
- for (let i = 0; i < words.length; i++) {
1748
- const head = literalWord(words[i]);
1749
- if (head === undefined || programBasename(head) !== "printf")
1750
- continue;
1751
- for (let j = i + 1; j < words.length; j++) {
1752
- const lit = literalWord(words[j]);
1753
- const opt = lit ?? words[j].pieces[0]?.literal ?? "";
1754
- if (lit === "--" || !opt.startsWith("-v"))
1755
- break;
1756
- if (lit === undefined)
1757
- return true;
1758
- const name = lit === "-v" ? words[++j] : undefined;
1759
- if (lit === "-v" && name === undefined)
1760
- break;
1761
- if (!IDENTIFIER.test(lit === "-v" ? (literalWord(name) ?? "") : lit.slice(2)))
1762
- return true;
1763
- }
1764
- }
1765
- return false;
1766
- }
1767
722
  function segmentCompoundForReadonly(command, homeDir) {
1768
723
  const trimmed = command.trim();
1769
724
  if (!trimmed)
@@ -1776,7 +731,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
1776
731
  if ("reject" in split)
1777
732
  return { reason: split.reject };
1778
733
  const { segments, pipeFed } = split;
1779
- const homeRebound = boundary?.homeDir !== undefined && homeMayBeRebound(command);
734
+ const homeRebound = boundary?.homeDir !== undefined && textBindsHome(command);
1780
735
  let nameRefusal;
1781
736
  const unlistedSegments = new Set();
1782
737
  const reheaded = new Map();
@@ -1791,7 +746,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
1791
746
  return { reason: "empty command" };
1792
747
  if (written === "coproc")
1793
748
  return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command" };
1794
- if (isBareControlWord(words, from, SHELL_BLOCK_WORDS)) {
749
+ if (isBareControlWord(words, from, BLOCK_WORD_NAMES)) {
1795
750
  unlistedSegments.add(si);
1796
751
  nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
1797
752
  continue;
@@ -1819,14 +774,14 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
1819
774
  continue;
1820
775
  }
1821
776
  nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
1822
- if (SHELL_REENTRY_PROGRAM_RE.test(name))
777
+ if (runsShellText(name))
1823
778
  return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text") };
1824
779
  if (pos.launchers.length > 0) {
1825
780
  const listed = program !== undefined && (allow.has(program.basename) || program.basename === "cd");
1826
- if (program !== undefined && !listed && SHELL_REENTRY_PROGRAM_RE.test(program.basename)) {
781
+ if (program !== undefined && !listed && runsShellText(program.basename)) {
1827
782
  return { reason: `"${written}" runs "${program.basename}", which runs its argument as shell text this classifier does not read ${NOT_AUTO_ALLOWED}` };
1828
783
  }
1829
- if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED)) {
784
+ if (argvReentryUnjudged(pos, opensNoOperand)) {
1830
785
  return { reason: `"${written}" takes the operands of the program it runs from its standard input, which this classifier does not read ${NOT_AUTO_ALLOWED}` };
1831
786
  }
1832
787
  if (!listed || program === undefined)
@@ -1838,34 +793,29 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
1838
793
  unlistedSegments.add(si);
1839
794
  }
1840
795
  const HEAD_AUTO_ALLOW_MAX = 1_000_000;
1841
- const headBoundIsSmall = (name, toks) => {
1842
- if (name !== "head")
796
+ const byteBoundIsSmall = (row, args) => {
797
+ if (row === undefined)
1843
798
  return false;
1844
799
  let byteBound = false;
1845
- for (let k = 1; k < toks.length; k++) {
1846
- const t = toks[k];
1847
- if (!t.startsWith("-") || t === "-")
800
+ for (const e of readArgRoles(row, args)) {
801
+ if (e.kind === "operand" || e.kind === "end-of-options")
1848
802
  continue;
1849
- const inline = /^-[A-Za-z]*c(\d+)$/.exec(t) ?? /^--bytes=(\d+)$/.exec(t);
1850
- if (inline) {
1851
- if (Number(inline[1]) > HEAD_AUTO_ALLOW_MAX)
803
+ if (e.kind === "value" && e.spec?.bounds === "bytes") {
804
+ if (!/^\d+$/.test(args[e.at]) || Number(args[e.at]) > HEAD_AUTO_ALLOW_MAX)
1852
805
  return false;
1853
806
  byteBound = true;
1854
807
  continue;
1855
808
  }
1856
- if (/^-[A-Za-z]*c$/.test(t) || t === "--bytes") {
1857
- const v = toks[k + 1];
1858
- if (v === undefined || !/^\d+$/.test(v) || Number(v) > HEAD_AUTO_ALLOW_MAX)
809
+ if (e.kind === "option" && typeof e.reading === "object" && e.reading.spec?.bounds === "bytes") {
810
+ if (!/^\d+$/.test(e.reading.attached) || Number(e.reading.attached) > HEAD_AUTO_ALLOW_MAX)
1859
811
  return false;
1860
812
  byteBound = true;
1861
- k++;
1862
813
  continue;
1863
814
  }
1864
815
  return false;
1865
816
  }
1866
817
  return byteBound;
1867
818
  };
1868
- const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity, diff: 2, cmp: 2, comm: 2, sed: 2 };
1869
819
  const foldedSegments = [];
1870
820
  let shapeRefusal = nameRefusal;
1871
821
  for (let si = 0; si < segments.length; si++) {
@@ -1881,35 +831,38 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
1881
831
  continue;
1882
832
  foldedSegments.push(segmentTokens);
1883
833
  const name = toks[0];
1884
- const guardReason = guardedVerbFormReason(name, toks, argWords);
834
+ const guardReason = guardedVerbFormReason(name, toks, argWords) ?? (writesWordsItIsHanded(programRow(name), toks.slice(1)) ? `\`${name}\` writes a file it is handed — not a read ${NOT_AUTO_ALLOWED}` : undefined);
1885
835
  if (guardReason !== undefined) {
1886
836
  shapeRefusal ??= guardReason;
1887
837
  continue;
1888
838
  }
839
+ const row = programRow(name);
840
+ const restArgs = toks.slice(1);
1889
841
  if (!(pipeFed[si] ?? false)) {
1890
- const floor = STDIN_FILE_FLOOR[name];
1891
- const restArgs = toks.slice(1);
1892
- const floorFirstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
1893
- const floorSlotSuppliedByFlag = floorFirstOperandSlot !== undefined && restArgs.some((t) => suppliesFirstOperandSlot(floorFirstOperandSlot, t));
1894
- let sawNonFlagOperand = false;
1895
- let hasStdinDash = false;
1896
- let operandCount = 0;
1897
- let endOfOptions = false;
1898
- if (name !== "tr") {
842
+ const floor = row?.operands?.stdinBelow;
843
+ if (floor === "always") {
844
+ shapeRefusal ??= `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
845
+ continue;
846
+ }
847
+ if (floor !== undefined) {
848
+ const floorFirstOperandSlot = firstOperandSlot(row);
849
+ const floorSlotSuppliedByFlag = floorFirstOperandSlot !== undefined && restArgs.some((t) => suppliesFirstOperandSlot(floorFirstOperandSlot, t));
850
+ let sawNonFlagOperand = false;
851
+ let hasStdinDash = false;
852
+ let operandCount = 0;
853
+ let endOfOptions = false;
1899
854
  for (let k = 0; k < restArgs.length; k++) {
1900
855
  const t = restArgs[k];
1901
- if (endOfOptions) {
1902
- }
1903
- else if (t === "--") {
1904
- endOfOptions = true;
1905
- continue;
1906
- }
1907
- else if (takesSeparatedValue(name, t)) {
1908
- k++;
1909
- continue;
1910
- }
1911
- else {
1912
- if (name === "grep" && (isGrepClusterFileStdin(t) || isGrepFileStdinLongOption(t))) {
856
+ if (!endOfOptions) {
857
+ if (t === "--") {
858
+ endOfOptions = true;
859
+ continue;
860
+ }
861
+ if (takesSeparatedDatum(row, t)) {
862
+ k++;
863
+ continue;
864
+ }
865
+ if (attachedStdinFile(row, t)) {
1913
866
  hasStdinDash = true;
1914
867
  break;
1915
868
  }
@@ -1926,31 +879,24 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
1926
879
  break;
1927
880
  }
1928
881
  }
1929
- }
1930
- if (floor !== undefined && hasStdinDash) {
1931
- shapeRefusal ??= `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
1932
- continue;
1933
- }
1934
- if (floor !== undefined && operandCount < floor) {
1935
- shapeRefusal ??= `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
1936
- continue;
882
+ if (hasStdinDash) {
883
+ shapeRefusal ??= `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
884
+ continue;
885
+ }
886
+ if (operandCount < floor) {
887
+ shapeRefusal ??= `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
888
+ continue;
889
+ }
1937
890
  }
1938
891
  }
1939
- if (name === "tail" && toks.slice(1).some((t) => t === "--follow" || t.startsWith("--follow=") || /^[-+][^\s]*[fF]/.test(t))) {
1940
- shapeRefusal ??= "`tail` in follow mode never terminates " + NOT_AUTO_ALLOWED;
892
+ if (selectedOptions(row, restArgs).some((o) => o.mode === "follows")) {
893
+ shapeRefusal ??= `\`${name}\` in follow mode never terminates ${NOT_AUTO_ALLOWED}`;
1941
894
  continue;
1942
895
  }
1943
896
  const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
1944
- const deviceCandidates = [];
1945
- for (const t of toks.slice(1)) {
1946
- if (t.startsWith("-") && t !== "-") {
1947
- deviceCandidates.push(...attachedOptionPayloads(t));
1948
- continue;
1949
- }
1950
- deviceCandidates.push(t);
1951
- }
897
+ const deviceCandidates = openableWordsOf(row, restArgs);
1952
898
  const deviceArgs = deviceCandidates.map((t) => resolveOperandLexically(undefined, t, boundary?.homeDir) ?? normalizeAbsPathLexically(t)).filter(isBlockedDevicePath);
1953
- const rescuedByHead = headBoundIsSmall(name, toks) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
899
+ const rescuedByHead = byteBoundIsSmall(row, restArgs) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
1954
900
  if (!rescuedByHead && deviceArgs.length > 0) {
1955
901
  shapeRefusal ??= "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED;
1956
902
  }
@@ -1960,8 +906,6 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
1960
906
  shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
1961
907
  return shapeRefusal !== undefined ? { reason: shapeRefusal } : {};
1962
908
  }
1963
- export const UNMODELLED_BASE_MOVERS = new Set(["pushd", "popd"]);
1964
- export const COMMAND_LAUNCHERS_FOR_TEST = COMMAND_LAUNCHERS;
1965
909
  export function classifyCompoundReadonly(command, allow, boundary) {
1966
910
  return classifyCompoundReadonlyDetailed(command, allow, boundary).reason;
1967
911
  }
@@ -2055,7 +999,7 @@ export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary
2055
999
  if (beats > POLL_LOOP_MAX_BEATS) {
2056
1000
  return { reason: `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow` };
2057
1001
  }
2058
- const homeRebound = boundary?.homeDir !== undefined && homeMayBeRebound(command);
1002
+ const homeRebound = boundary?.homeDir !== undefined && textBindsHome(command);
2059
1003
  const bodies = shape.body.map((b) => ({ ...b, spelled: segmentSpellings(b.words, boundary?.homeDir, homeRebound).raw }));
2060
1004
  for (const b of bodies) {
2061
1005
  if (b.segment.connector !== ";" || b.segment.tokens.some((t) => t.kind !== "word") || b.spelled.some((raw) => POLL_BODY_HARD_REJECT.test(raw))) {