@taliesinb/mash 0.0.0-stage → 0.1.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 (203) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -2
  3. package/bin/mash.ts +253 -0
  4. package/dist/bin/mash.d.ts +39 -0
  5. package/dist/bin/mash.d.ts.map +1 -0
  6. package/dist/bin/mash.js +281 -0
  7. package/dist/bin/mash.js.map +1 -0
  8. package/dist/src/analysis/analyze.d.ts +97 -0
  9. package/dist/src/analysis/analyze.d.ts.map +1 -0
  10. package/dist/src/analysis/analyze.js +1406 -0
  11. package/dist/src/analysis/analyze.js.map +1 -0
  12. package/dist/src/analysis/fs.d.ts +39 -0
  13. package/dist/src/analysis/fs.d.ts.map +1 -0
  14. package/dist/src/analysis/fs.js +117 -0
  15. package/dist/src/analysis/fs.js.map +1 -0
  16. package/dist/src/analysis/outputs.d.ts +16 -0
  17. package/dist/src/analysis/outputs.d.ts.map +1 -0
  18. package/dist/src/analysis/outputs.js +277 -0
  19. package/dist/src/analysis/outputs.js.map +1 -0
  20. package/dist/src/analysis/value.d.ts +76 -0
  21. package/dist/src/analysis/value.d.ts.map +1 -0
  22. package/dist/src/analysis/value.js +248 -0
  23. package/dist/src/analysis/value.js.map +1 -0
  24. package/dist/src/behaviors/defs/core.d.ts +15 -0
  25. package/dist/src/behaviors/defs/core.d.ts.map +1 -0
  26. package/dist/src/behaviors/defs/core.js +1084 -0
  27. package/dist/src/behaviors/defs/core.js.map +1 -0
  28. package/dist/src/behaviors/defs/devtools.d.ts +9 -0
  29. package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
  30. package/dist/src/behaviors/defs/devtools.js +432 -0
  31. package/dist/src/behaviors/defs/devtools.js.map +1 -0
  32. package/dist/src/behaviors/defs/macos.d.ts +12 -0
  33. package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
  34. package/dist/src/behaviors/defs/macos.js +454 -0
  35. package/dist/src/behaviors/defs/macos.js.map +1 -0
  36. package/dist/src/behaviors/defs/tools.d.ts +9 -0
  37. package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
  38. package/dist/src/behaviors/defs/tools.js +639 -0
  39. package/dist/src/behaviors/defs/tools.js.map +1 -0
  40. package/dist/src/behaviors/dsl.d.ts +95 -0
  41. package/dist/src/behaviors/dsl.d.ts.map +1 -0
  42. package/dist/src/behaviors/dsl.js +267 -0
  43. package/dist/src/behaviors/dsl.js.map +1 -0
  44. package/dist/src/behaviors/index.d.ts +12 -0
  45. package/dist/src/behaviors/index.d.ts.map +1 -0
  46. package/dist/src/behaviors/index.js +18 -0
  47. package/dist/src/behaviors/index.js.map +1 -0
  48. package/dist/src/behaviors/registry.d.ts +45 -0
  49. package/dist/src/behaviors/registry.d.ts.map +1 -0
  50. package/dist/src/behaviors/registry.js +266 -0
  51. package/dist/src/behaviors/registry.js.map +1 -0
  52. package/dist/src/behaviors/roles.d.ts +29 -0
  53. package/dist/src/behaviors/roles.d.ts.map +1 -0
  54. package/dist/src/behaviors/roles.js +198 -0
  55. package/dist/src/behaviors/roles.js.map +1 -0
  56. package/dist/src/behaviors/types.d.ts +120 -0
  57. package/dist/src/behaviors/types.d.ts.map +1 -0
  58. package/dist/src/behaviors/types.js +12 -0
  59. package/dist/src/behaviors/types.js.map +1 -0
  60. package/dist/src/index.d.ts +27 -0
  61. package/dist/src/index.d.ts.map +1 -0
  62. package/dist/src/index.js +24 -0
  63. package/dist/src/index.js.map +1 -0
  64. package/dist/src/integrations/deepsea.d.ts +121 -0
  65. package/dist/src/integrations/deepsea.d.ts.map +1 -0
  66. package/dist/src/integrations/deepsea.js +199 -0
  67. package/dist/src/integrations/deepsea.js.map +1 -0
  68. package/dist/src/machine.d.ts +139 -0
  69. package/dist/src/machine.d.ts.map +1 -0
  70. package/dist/src/machine.js +400 -0
  71. package/dist/src/machine.js.map +1 -0
  72. package/dist/src/parser/ast.d.ts +369 -0
  73. package/dist/src/parser/ast.d.ts.map +1 -0
  74. package/dist/src/parser/ast.js +17 -0
  75. package/dist/src/parser/ast.js.map +1 -0
  76. package/dist/src/parser/parser.d.ts +48 -0
  77. package/dist/src/parser/parser.d.ts.map +1 -0
  78. package/dist/src/parser/parser.js +1950 -0
  79. package/dist/src/parser/parser.js.map +1 -0
  80. package/dist/src/policy/effects.d.ts +86 -0
  81. package/dist/src/policy/effects.d.ts.map +1 -0
  82. package/dist/src/policy/effects.js +53 -0
  83. package/dist/src/policy/effects.js.map +1 -0
  84. package/dist/src/policy/policy.d.ts +130 -0
  85. package/dist/src/policy/policy.d.ts.map +1 -0
  86. package/dist/src/policy/policy.js +532 -0
  87. package/dist/src/policy/policy.js.map +1 -0
  88. package/dist/src/printer/doc.d.ts +151 -0
  89. package/dist/src/printer/doc.d.ts.map +1 -0
  90. package/dist/src/printer/doc.js +409 -0
  91. package/dist/src/printer/doc.js.map +1 -0
  92. package/dist/src/printer/highlight.d.ts +53 -0
  93. package/dist/src/printer/highlight.d.ts.map +1 -0
  94. package/dist/src/printer/highlight.js +339 -0
  95. package/dist/src/printer/highlight.js.map +1 -0
  96. package/dist/src/printer/html.d.ts +72 -0
  97. package/dist/src/printer/html.d.ts.map +1 -0
  98. package/dist/src/printer/html.js +423 -0
  99. package/dist/src/printer/html.js.map +1 -0
  100. package/dist/src/printer/print.d.ts +63 -0
  101. package/dist/src/printer/print.d.ts.map +1 -0
  102. package/dist/src/printer/print.js +484 -0
  103. package/dist/src/printer/print.js.map +1 -0
  104. package/dist/src/printer/summary.d.ts +50 -0
  105. package/dist/src/printer/summary.d.ts.map +1 -0
  106. package/dist/src/printer/summary.js +621 -0
  107. package/dist/src/printer/summary.js.map +1 -0
  108. package/dist/src/trace/events.d.ts +183 -0
  109. package/dist/src/trace/events.d.ts.map +1 -0
  110. package/dist/src/trace/events.js +9 -0
  111. package/dist/src/trace/events.js.map +1 -0
  112. package/dist/src/trace/progress.d.ts +58 -0
  113. package/dist/src/trace/progress.d.ts.map +1 -0
  114. package/dist/src/trace/progress.js +83 -0
  115. package/dist/src/trace/progress.js.map +1 -0
  116. package/dist/src/vm/arith.d.ts +29 -0
  117. package/dist/src/vm/arith.d.ts.map +1 -0
  118. package/dist/src/vm/arith.js +258 -0
  119. package/dist/src/vm/arith.js.map +1 -0
  120. package/dist/src/vm/builtins.d.ts +31 -0
  121. package/dist/src/vm/builtins.d.ts.map +1 -0
  122. package/dist/src/vm/builtins.js +1773 -0
  123. package/dist/src/vm/builtins.js.map +1 -0
  124. package/dist/src/vm/emulate.d.ts +75 -0
  125. package/dist/src/vm/emulate.d.ts.map +1 -0
  126. package/dist/src/vm/emulate.js +380 -0
  127. package/dist/src/vm/emulate.js.map +1 -0
  128. package/dist/src/vm/expand.d.ts +68 -0
  129. package/dist/src/vm/expand.d.ts.map +1 -0
  130. package/dist/src/vm/expand.js +744 -0
  131. package/dist/src/vm/expand.js.map +1 -0
  132. package/dist/src/vm/external.d.ts +34 -0
  133. package/dist/src/vm/external.d.ts.map +1 -0
  134. package/dist/src/vm/external.js +228 -0
  135. package/dist/src/vm/external.js.map +1 -0
  136. package/dist/src/vm/fifo.d.ts +66 -0
  137. package/dist/src/vm/fifo.d.ts.map +1 -0
  138. package/dist/src/vm/fifo.js +232 -0
  139. package/dist/src/vm/fifo.js.map +1 -0
  140. package/dist/src/vm/interp.d.ts +209 -0
  141. package/dist/src/vm/interp.d.ts.map +1 -0
  142. package/dist/src/vm/interp.js +1641 -0
  143. package/dist/src/vm/interp.js.map +1 -0
  144. package/dist/src/vm/io.d.ts +180 -0
  145. package/dist/src/vm/io.d.ts.map +1 -0
  146. package/dist/src/vm/io.js +502 -0
  147. package/dist/src/vm/io.js.map +1 -0
  148. package/dist/src/vm/pattern.d.ts +46 -0
  149. package/dist/src/vm/pattern.d.ts.map +1 -0
  150. package/dist/src/vm/pattern.js +498 -0
  151. package/dist/src/vm/pattern.js.map +1 -0
  152. package/dist/src/vm/proc.d.ts +94 -0
  153. package/dist/src/vm/proc.d.ts.map +1 -0
  154. package/dist/src/vm/proc.js +130 -0
  155. package/dist/src/vm/proc.js.map +1 -0
  156. package/dist/src/vm/state.d.ts +104 -0
  157. package/dist/src/vm/state.d.ts.map +1 -0
  158. package/dist/src/vm/state.js +233 -0
  159. package/dist/src/vm/state.js.map +1 -0
  160. package/dist/src/vm/test.d.ts +11 -0
  161. package/dist/src/vm/test.d.ts.map +1 -0
  162. package/dist/src/vm/test.js +277 -0
  163. package/dist/src/vm/test.js.map +1 -0
  164. package/package.json +63 -4
  165. package/src/analysis/analyze.ts +1395 -0
  166. package/src/analysis/fs.ts +121 -0
  167. package/src/analysis/outputs.ts +242 -0
  168. package/src/analysis/value.ts +264 -0
  169. package/src/behaviors/defs/core.ts +970 -0
  170. package/src/behaviors/defs/devtools.ts +400 -0
  171. package/src/behaviors/defs/macos.ts +426 -0
  172. package/src/behaviors/defs/tools.ts +596 -0
  173. package/src/behaviors/dsl.ts +293 -0
  174. package/src/behaviors/index.ts +19 -0
  175. package/src/behaviors/registry.ts +261 -0
  176. package/src/behaviors/roles.ts +187 -0
  177. package/src/behaviors/types.ts +127 -0
  178. package/src/index.ts +27 -0
  179. package/src/integrations/deepsea.ts +259 -0
  180. package/src/machine.ts +483 -0
  181. package/src/parser/ast.ts +472 -0
  182. package/src/parser/parser.ts +1826 -0
  183. package/src/policy/effects.ts +102 -0
  184. package/src/policy/policy.ts +578 -0
  185. package/src/printer/doc.ts +454 -0
  186. package/src/printer/highlight.ts +283 -0
  187. package/src/printer/html.ts +441 -0
  188. package/src/printer/print.ts +528 -0
  189. package/src/printer/summary.ts +601 -0
  190. package/src/trace/events.ts +102 -0
  191. package/src/trace/progress.ts +119 -0
  192. package/src/vm/arith.ts +236 -0
  193. package/src/vm/builtins.ts +1590 -0
  194. package/src/vm/emulate.ts +337 -0
  195. package/src/vm/expand.ts +721 -0
  196. package/src/vm/external.ts +265 -0
  197. package/src/vm/fifo.ts +226 -0
  198. package/src/vm/interp.ts +1623 -0
  199. package/src/vm/io.ts +570 -0
  200. package/src/vm/pattern.ts +477 -0
  201. package/src/vm/proc.ts +147 -0
  202. package/src/vm/state.ts +267 -0
  203. package/src/vm/test.ts +253 -0
@@ -0,0 +1,1084 @@
1
+ /**
2
+ * Behavior definitions for common POSIX/GNU utilities and developer tools.
3
+ *
4
+ * Conventions:
5
+ * - Paths are resolved with `h.resolve` (relative to the invocation cwd).
6
+ * - Unknown flags or opaque arguments set `complete: false` rather than
7
+ * guessing; the policy then treats the command's effects as unknown.
8
+ * - `readsStdin` is false when the command has file operands (or never reads stdin).
9
+ */
10
+ import * as fs from 'node:fs';
11
+ import { concat, concrete, hole, known, knownPrefix } from "../../analysis/value.js";
12
+ import { analyzeDef, define } from "../dsl.js";
13
+ const args = (inv) => inv.argv.slice(1);
14
+ function incomplete(p) {
15
+ return p.unknownFlags.length > 0;
16
+ }
17
+ const isStdinOperand = (v) => concrete(v) === '-';
18
+ /** A filter that reads its file operands (or stdin when there are none) and writes stdout. */
19
+ function reader(names, spec, summary, more = {}) {
20
+ return define({ names, summary, spec, operands: 'read*', ...more });
21
+ }
22
+ /** Commands with no file-system effects that never read stdin (arguments are not inspected). */
23
+ function pure(names, summary, stdout) {
24
+ return { names, summary, analyze: () => ({ effects: [], readsStdin: false, stdout }) };
25
+ }
26
+ const BSD = process.platform === 'darwin';
27
+ const SSH_SPEC = { short: '46AaCfGgKkMNnqsTtVvXxYy', shortWithArg: 'BbcDEeFIiJLlmOoPpQRSWw', posix: true };
28
+ const NET_CLIENTS = {
29
+ ssh: SSH_SPEC,
30
+ scp: { short: '346ABCOpqRrTv', shortWithArg: 'cDFiJloPSX' },
31
+ sftp: { short: '46AaCfNpqrv', shortWithArg: 'BbcDFiJloPRSsX' },
32
+ nc: { short: '46DdhklnrStUuvzNCF', shortWithArg: 'GwpsxXIOiTVMmeEq' },
33
+ netcat: { short: '46DdhklnrStUuvzNCF', shortWithArg: 'GwpsxXIOiTVMmeEq' },
34
+ telnet: { short: '78EFKLNacdfrx', shortWithArg: 'elnbSX' },
35
+ ftp: { short: '46AadegimnpRtVv', shortWithArg: 'oPqrsTu' },
36
+ ping: { short: '46aAdDfLnoqQrRvU', shortWithArg: 'bcCgGhiIlmMpPsStTwWzk' },
37
+ ping6: { short: 'aAdDfHmnNoqtvwyY', shortWithArg: 'bcgGhiIlmpPsSzk' },
38
+ traceroute: { short: '46adeFIlnrSvxEP', shortWithArg: 'AfgimMpPqsStwz' },
39
+ traceroute6: { short: 'adIlnrSvU', shortWithArg: 'fgimpqstw' },
40
+ dig: { short: '46hiruvm', shortWithArg: 'bcfkpqtxy' },
41
+ host: { short: 'aACdilrsTUvVw46', shortWithArg: 'cNRtWm' },
42
+ nslookup: { longSingle: true, long: { debug: false, d2: false, nodebug: false } },
43
+ };
44
+ const CHMOD = {
45
+ spec: { short: 'RfvchHLPN', long: { recursive: '=R', reference: true, verbose: false, changes: false, silent: false, quiet: false, 'no-dereference': false, dereference: false, 'preserve-root': false, 'no-preserve-root': false, from: true } },
46
+ operands: 'arg write*', recursive: ['R'], stdin: false,
47
+ when: { reference: { operands: 'write*', flags: { reference: 'read' } } },
48
+ };
49
+ const HEAD_TAIL = { short: 'qvzfFr', shortWithArg: 'ncsb', numeric: 'n', long: { lines: '=n', bytes: '=c', follow: false, quiet: false, silent: false, verbose: false, retry: false, pid: true, 'sleep-interval': '=s', 'zero-terminated': false } };
50
+ export const coreBehaviors = [
51
+ pure(['true', 'false', ':'], 'no effect'),
52
+ pure(['echo', 'printf'], 'write arguments to stdout'),
53
+ pure(['sleep'], 'wait'),
54
+ pure(['seq'], 'print a sequence', { type: 'int', lines: true }),
55
+ pure(['date'], 'print the date'),
56
+ pure(['whoami', 'id', 'groups', 'logname', 'who', 'w', 'users', 'last'], 'print user identity'),
57
+ pure(['uname', 'hostname', 'arch', 'nproc', 'getconf', 'sw_vers', 'tty', 'locale', 'uptime', 'vm_stat', 'iostat', 'free'], 'print system information'),
58
+ pure(['ps', 'top', 'pstree', 'lsof', 'netstat'], 'list processes and open files'),
59
+ pure(['printenv'], 'print environment'),
60
+ pure(['expr', 'factor'], 'evaluate an expression'),
61
+ pure(['yes'], 'repeat a string'),
62
+ pure(['uuidgen'], 'print a UUID', { type: 'string' }),
63
+ pure(['clear', 'tput', 'stty', 'reset'], 'terminal control'),
64
+ pure(['which', 'type', 'command', 'whereis'], 'locate a command', { type: 'path' }),
65
+ {
66
+ names: ['basename'],
67
+ summary: 'strip directory and suffix',
68
+ analyze: () => ({ effects: [], readsStdin: false, stdout: { type: 'filename' } }),
69
+ },
70
+ {
71
+ names: ['dirname'],
72
+ summary: 'strip last component',
73
+ analyze: () => ({ effects: [], readsStdin: false, stdout: { type: 'path' } }),
74
+ },
75
+ {
76
+ names: ['pwd'],
77
+ summary: 'print working directory',
78
+ analyze: (inv) => ({ effects: [], readsStdin: false, stdout: { type: 'path', value: inv.cwd } }),
79
+ },
80
+ define({
81
+ names: ['realpath', 'readlink'], summary: 'resolve paths',
82
+ spec: { short: 'esmqzfnLP', long: { canonicalize: false, 'canonicalize-existing': false, 'canonicalize-missing': false, 'no-newline': false, relative: true, 'relative-to': true, 'relative-base': true, quiet: false, zero: false } },
83
+ operands: 'read*', stdin: false, stdout: { type: 'path' },
84
+ }),
85
+ define({
86
+ names: ['pgrep'], summary: 'find processes by name',
87
+ spec: { short: 'flvxinoaciLAIwr', shortWithArg: 'dgGPsuUtFqS', long: { full: false, 'list-name': false, 'list-full': false, exact: false, 'ignore-case': false, newest: false, oldest: false, count: false, inverse: false, delimiter: '=d', parent: '=P', uid: '=u', euid: '=U', terminal: '=t', pidfile: '=F', signal: true } },
88
+ operands: 'pattern*', flags: { F: 'read' }, stdin: false,
89
+ }),
90
+ define({
91
+ names: ['bc'], summary: 'calculator (reads its files, then stdin)',
92
+ spec: { short: 'hilqsvwcCLP', shortWithArg: 'e', long: { mathlib: false, quiet: false, standard: false, warn: false, version: false, help: false, interactive: false, expression: '=e' } },
93
+ operands: 'read*', stdin: true, flags: { e: 'code' },
94
+ }),
95
+ pure(['envsubst'], 'substitute environment variables (stdin to stdout)'),
96
+ reader(['cat', 'tac', 'nl', 'rev', 'fold', 'fmt', 'strings', 'hexdump', 'more', 'less', 'bat', 'expand', 'unexpand'], { short: 'AbeEnstTuvrwqpSlc', shortWithArg: 'wnN', long: { number: false, 'show-all': false, squeeze: false, 'number-nonblank': false, 'show-ends': false, 'show-tabs': false, plain: false, paging: true, language: true, style: true, 'line-range': true, color: true, width: true, tabs: true, all: false, initial: false } }, 'read files, write stdout'),
97
+ reader(['od'], { short: 'bcdDfFhHiIlLoOsvxX', shortWithArg: 'AjNtwS', long: { 'address-radix': '=A', 'skip-bytes': '=j', 'read-bytes': '=N', format: '=t', 'output-duplicates': false, width: true, endian: true } }, 'dump files'),
98
+ // xxd INFILE [OUTFILE]; -r reverses (still writes OUTFILE)
99
+ define({ names: ['xxd'], summary: 'hex dump (xxd [in [out]])', spec: { short: 'abCeEhiprRuv', shortWithArg: 'cglosn', long: { autoskip: false, bits: false, include: false, plain: false, revert: false, uppercase: false, cols: '=c', len: '=l', seek: '=s', name: '=n' } }, operands: 'read write?' }),
100
+ reader(['column'], { short: 'tJLnxerRlH', shortWithArg: 'sOcoNWdET', long: { table: false, separator: '=s', 'output-separator': '=o', 'output-width': '=c', 'table-columns': '=N', json: false } }, 'format columns'),
101
+ // GNU base64 [-d] [-w N] [FILE]; BSD base64 [-dDh] [-b N] [-i IN] [-o OUT]
102
+ reader(['base64', 'base32', 'basenc'], { short: 'dDhn', shortWithArg: 'wbio', long: { decode: false, 'ignore-garbage': false, wrap: '=w', input: '=i', output: '=o', break: '=b', base64: false, base64url: false, base32: false, base32hex: false, base16: false } }, 'encode / decode', { flags: { i: 'read', o: 'write' }, when: { i: { stdin: false } } }),
103
+ reader(['head', 'tail'], HEAD_TAIL, 'print part of files'),
104
+ reader(['wc'], { short: 'clmwL', long: { bytes: false, chars: false, lines: false, words: false, 'max-line-length': false } }, 'count', { stdout: { type: 'string' } }),
105
+ reader(['cut'], { short: 'snzw', shortWithArg: 'bcdf', long: { bytes: '=b', characters: '=c', delimiter: '=d', fields: '=f', complement: false, 'only-delimited': false, 'output-delimiter': true, 'zero-terminated': false } }, 'cut fields'),
106
+ reader(['paste'], { short: 'sz', shortWithArg: 'd', long: { delimiters: '=d', serial: false } }, 'merge lines'),
107
+ reader(['shuf'], { short: 'erzn', shortWithArg: 'inoS', long: { echo: false, 'input-range': '=i', 'head-count': '=n', output: '=o', 'random-source': true, repeat: false } }, 'shuffle lines', { flags: { o: 'write', 'random-source': 'read' }, when: { e: { operands: 'arg*', stdin: false }, i: { operands: '', stdin: false } } }),
108
+ reader(['iconv'], { short: 'csl', shortWithArg: 'fto', long: { 'from-code': '=f', 'to-code': '=t', output: '=o', list: false, silent: false } }, 'convert encodings', { flags: { o: 'write' } }),
109
+ reader(['join'], { short: 'iz', shortWithArg: 'ajoetv12', long: { 'ignore-case': false, 'check-order': false, 'nocheck-order': false, header: false } }, 'join sorted files'),
110
+ define({
111
+ names: ['uniq'], summary: 'filter repeated lines (uniq [IN [OUT]])',
112
+ spec: { short: 'cdDiuz', shortWithArg: 'fsw', long: { count: false, repeated: false, unique: false, 'ignore-case': false, 'skip-fields': '=f', 'skip-chars': '=s', 'check-chars': '=w', 'all-repeated': true, group: true, 'zero-terminated': false } },
113
+ operands: 'read? write?',
114
+ // OUT of `-` is stdout
115
+ extra: (r, { operands }) => (operands[1] && isStdinOperand(operands[1]) ? { ...r, effects: r.effects.filter((e) => e.kind !== 'write') } : r),
116
+ }),
117
+ {
118
+ names: ['csplit'],
119
+ summary: 'split a file at patterns into pieces named PREFIXnn',
120
+ analyze(inv, h) {
121
+ const p = h.parseArgs(args(inv), { short: 'ksqz', shortWithArg: 'fbn', long: { prefix: '=f', 'suffix-format': '=b', digits: '=n', 'keep-files': '=k', quiet: '=q', silent: '=s', 'elide-empty-files': '=z', 'suppress-matched': false } });
122
+ const [input, ...patterns] = p.operands;
123
+ for (const pat of patterns)
124
+ h.role(pat, 'pattern');
125
+ const effects = [];
126
+ if (input && !isStdinOperand(input))
127
+ effects.push({ kind: 'read', path: h.resolve(input) });
128
+ const f = p.flags.get('f');
129
+ const target = h.resolve(f && f !== true ? f : known('xx'));
130
+ effects.push({ kind: 'write', path: { alts: target.alts.map((a) => [...a, { hole: true, type: 'filename', origin: 'csplit suffix' }]) } });
131
+ return { effects, readsStdin: !input || isStdinOperand(input), complete: !incomplete(p) };
132
+ },
133
+ },
134
+ {
135
+ names: ['split'],
136
+ summary: 'split a file into pieces named PREFIX*',
137
+ analyze(inv, h) {
138
+ const p = h.parseArgs(args(inv), { short: 'dxe', shortWithArg: 'lbnCa', long: { lines: true, bytes: true, number: true, 'line-bytes': true, 'suffix-length': true, 'numeric-suffixes': false, 'additional-suffix': true, verbose: false } });
139
+ const [input, prefix] = p.operands;
140
+ const effects = [];
141
+ if (input && !isStdinOperand(input))
142
+ effects.push({ kind: 'read', path: h.resolve(input) });
143
+ const pre = prefix ?? known('x');
144
+ const target = h.resolve(pre);
145
+ effects.push({ kind: 'write', path: { alts: target.alts.map((a) => [...a, { hole: true, type: 'filename', origin: 'split suffix' }]) } });
146
+ return { effects, readsStdin: !input || isStdinOperand(input), complete: !incomplete(p) };
147
+ },
148
+ },
149
+ reader(['comm'], { short: '123z', long: { 'check-order': false, 'nocheck-order': false, 'output-delimiter': true, total: false } }, 'compare sorted files'),
150
+ reader(['diff', 'cmp'], { short: 'uqrNbBiwasylcntTpdEZ', shortWithArg: 'UCxXIFS', long: { unified: true, recursive: false, brief: false, color: true, 'ignore-all-space': false, 'ignore-space-change': false, 'ignore-blank-lines': false, 'ignore-case': false, exclude: true, 'exclude-from': '=X', text: false, 'new-file': false, 'side-by-side': false, 'strip-trailing-cr': false, label: true, 'no-dereference': false, normal: false, minimal: false, silent: false, quiet: false, 'ignore-matching-lines': '=I', 'show-function-line': '=F', 'starting-file': '=S', 'width': true, 'suppress-common-lines': false } }, 'compare files', { flags: { X: 'read' }, recursive: ['r', 'recursive'] }),
151
+ reader(['md5sum', 'sha1sum', 'sha224sum', 'sha256sum', 'sha384sum', 'sha512sum', 'shasum', 'cksum', 'b2sum', 'b3sum', 'sum'], { short: 'bctwzU0sq', shortWithArg: 'aol', long: { check: false, binary: false, text: false, algorithm: '=a', tag: false, quiet: false, status: false, strict: false, warn: false, zero: false, length: '=l', untagged: false, 'ignore-missing': false, raw: false, base64: false } }, 'checksum files'),
152
+ // BSD md5: -s STRING checksums a string
153
+ reader(['md5'], { short: 'pqrtxn', shortWithArg: 's' }, 'checksum files', { when: { s: { stdin: false } } }),
154
+ reader(['file'], { short: 'bcdhiklLNnprsvzZ0SI', shortWithArg: 'fmFeP', long: { brief: false, 'mime-type': false, 'mime-encoding': false, mime: false, dereference: false, 'no-dereference': false, 'files-from': '=f', 'magic-file': '=m', separator: '=F', exclude: '=e', 'keep-going': false, raw: false, special: false, uncompress: false } }, 'inspect files', { flags: { f: 'read', m: 'read' }, stdin: false }),
155
+ // BSD stat: -f FORMAT, -t TIMEFMT; GNU stat: -c FORMAT, -f = file-system status
156
+ reader(['stat'], BSD
157
+ ? { short: 'FLlnqrsx', shortWithArg: 'ftc', long: { format: '=c', dereference: false } }
158
+ : { short: 'Lfte', shortWithArg: 'c', long: { format: '=c', printf: true, dereference: false, 'file-system': false, terse: false, cached: true } }, 'inspect files', { stdin: false }),
159
+ reader(['tr'], { short: 'cdsCt', long: { complement: false, delete: false, 'squeeze-repeats': false, truncate: false } }, 'translate characters', { operands: 'arg*', stdin: true }),
160
+ define({
161
+ names: ['jq', 'gojq', 'jaq'], summary: 'JSON processor',
162
+ spec: { short: 'rjcnsSeaCMRxh', shortWithArg: 'fL', long: { arg: 2, argjson: 2, slurpfile: 2, rawfile: 2, args: false, jsonargs: false, 'raw-output': false, 'raw-output0': false, 'join-output': false, 'ascii-output': false, compact: false, 'null-input': false, 'raw-input': false, slurp: false, tab: false, indent: true, 'sort-keys': false, 'color-output': false, 'monochrome-output': false, 'exit-status': false, 'from-file': '=f', seq: false, stream: false, 'stream-errors': false } },
163
+ operands: 'code read*', flags: { f: 'read', slurpfile: 'read', rawfile: 'read' },
164
+ when: { f: { operands: 'read*' }, 'args|jsonargs': { operands: 'code arg*', stdin: true }, n: { stdin: false } },
165
+ }),
166
+ // grep needs special handling: the first operand is the pattern unless -e/-f
167
+ {
168
+ names: ['grep', 'egrep', 'fgrep', 'rg', 'ag'],
169
+ summary: 'search files',
170
+ analyze(inv, h) {
171
+ const rg = inv.name === 'rg' || inv.name === 'ag';
172
+ const p = h.parseArgs(args(inv), {
173
+ short: rg ? 'iFwxclLnNHosqvuUzSa.P' : 'EFGPivwxyclLnhHosqbrRUzZaIT',
174
+ shortWithArg: rg ? 'efgtTmABCjMrE' : 'efmABCdD',
175
+ long: { regexp: true, file: true, 'ignore-case': false, 'invert-match': false, 'word-regexp': false, 'line-regexp': false, count: false, 'files-with-matches': false, 'files-without-match': false, 'line-number': false, 'no-filename': false, 'with-filename': false, 'only-matching': false, quiet: false, silent: false, recursive: false, 'dereference-recursive': false, include: true, exclude: true, 'exclude-dir': true, 'exclude-from': true, color: true, colour: true, 'max-count': true, context: true, 'after-context': true, 'before-context': true, 'extended-regexp': false, 'fixed-strings': false, 'perl-regexp': false, 'null-data': false, binary: false, text: false, glob: true, iglob: true, type: true, 'type-not': true, 'type-add': true, hidden: false, 'no-ignore': false, 'no-ignore-vcs': false, json: false, 'files': false, 'smart-case': false, 'max-depth': true, 'no-heading': false, heading: false, 'no-messages': false, 'line-buffered': false, multiline: false, 'multiline-dotall': false, replace: true, 'sort': true, 'sortr': true, 'follow': false, 'max-columns': true, 'max-filesize': true, threads: true, 'column': false, 'vimgrep': false, 'stats': false, 'pcre2': false, 'null': false, 'byte-offset': false, 'trim': false, 'passthru': false, 'unrestricted': false, 'no-config': false, 'count-matches': false, 'ignore-file': true, 'label': true, 'initial-tab': false, 'devices': true, 'directories': true, 'binary-files': true }
176
+ });
177
+ const operands = [...p.operands];
178
+ for (const e of [...(p.multi.get('e') ?? []), ...(p.multi.get('regexp') ?? [])])
179
+ h.role(e, 'pattern');
180
+ if (!p.flags.has('e') && !p.flags.has('f') && !p.flags.has('regexp') && !p.flags.has('file') && !(rg && (p.flags.has('files') || p.flags.has('type-list')))) {
181
+ const pat = operands.shift();
182
+ if (pat)
183
+ h.role(pat, 'pattern');
184
+ }
185
+ const recursive = rg || p.flags.has('r') || p.flags.has('R') || p.flags.has('recursive') || p.flags.has('dereference-recursive');
186
+ const files = operands.filter((f) => !isStdinOperand(f));
187
+ const effects = files.map((f) => ({ kind: 'read', path: h.resolve(f), recursive }));
188
+ // rg and grep -r search the cwd without operands
189
+ if (operands.length === 0 && recursive)
190
+ effects.push({ kind: 'read', path: inv.cwd, recursive: true });
191
+ const pf = p.flags.get('f') ?? p.flags.get('file');
192
+ if (pf && pf !== true && !isStdinOperand(pf))
193
+ effects.push({ kind: 'read', path: h.resolve(pf) });
194
+ const readsStdin = !rg && !recursive ? operands.length === 0 || operands.some(isStdinOperand) : false;
195
+ return { effects, readsStdin, complete: !incomplete(p) };
196
+ },
197
+ },
198
+ define({
199
+ names: ['sort'], summary: 'sort lines',
200
+ spec: { short: 'bdfgiMhnRrscCumVz', shortWithArg: 'kotST', long: { output: '=o', key: '=k', 'field-separator': '=t', reverse: false, numeric: false, 'numeric-sort': false, unique: false, 'human-numeric-sort': false, 'version-sort': false, 'general-numeric-sort': false, 'ignore-case': false, 'month-sort': false, 'random-sort': false, 'ignore-leading-blanks': false, 'dictionary-order': false, check: false, merge: false, parallel: true, 'buffer-size': '=S', 'temporary-directory': '=T', stable: false, 'zero-terminated': false, 'files0-from': true, 'random-source': true, debug: false } },
201
+ operands: 'read*', flags: { o: 'write', T: 'write:dir~', 'files0-from': 'read', 'random-source': 'read' },
202
+ }),
203
+ define({
204
+ names: ['tee'], summary: 'copy stdin to files',
205
+ spec: { short: 'aip', long: { append: '=a', 'ignore-interrupts': '=i', 'output-error': true } },
206
+ operands: 'write*', stdin: true, when: { a: { operands: 'append*' } },
207
+ }),
208
+ define({
209
+ names: ['ls'], summary: 'list directories', namesOnly: true,
210
+ spec: { short: '1aAbBcCdfFgGhHiklLmnNopqQrRsStTuUvwxXZ@eO', shortWithArg: BSD ? 'IDP' : 'IP', long: { all: false, 'almost-all': false, color: true, classify: false, human: false, 'human-readable': false, recursive: false, directory: false, 'block-size': true, 'group-directories-first': false, time: true, 'time-style': true, 'full-time': false, sort: true, ignore: true, 'hide': true, si: false, inode: false, 'dereference': false, 'quoting-style': true, literal: false, width: true, format: true, 'indicator-style': true, 'hyperlink': true } },
211
+ operands: 'read*', cwdIfNone: true, recursive: ['R', 'recursive'], stdin: false,
212
+ // operands are usually directories (a name with an extension is more likely a file)
213
+ extra: (r, { h, operands }) => {
214
+ for (const f of operands)
215
+ if (!/\.[A-Za-z]\w{0,7}$/.test(concrete(f) ?? ''))
216
+ h.role(f, 'dir');
217
+ return r;
218
+ },
219
+ }),
220
+ define({
221
+ names: ['tree', 'du'], summary: 'list directory trees', namesOnly: true,
222
+ spec: { short: '1aAbBcCdfFgGhHiklLmnNopqQrRsStTuUvwxXZ@eDJ', shortWithArg: 'IdLPt', long: { all: false, 'max-depth': '=d', summarize: false, total: false, 'block-size': true, human: false, 'human-readable': false, si: false, apparent: false, 'apparent-size': false, exclude: true, 'one-file-system': false, 'dirsfirst': false, 'noreport': false, charset: true, 'gitignore': false, threshold: '=t', 'time': true, inodes: false, 'null': false, 'count-links': false, 'dereference': false, 'exclude-from': true, 'files0-from': true } },
223
+ operands: 'read*', cwdIfNone: true, recursive: true, stdin: false,
224
+ }),
225
+ pure(['df'], 'report disk usage'),
226
+ // ---- writers ----
227
+ define({
228
+ names: ['touch'], summary: 'create files / update times',
229
+ spec: { short: 'acmfh', shortWithArg: 'rtdA', long: { 'no-create': false, date: '=d', reference: '=r', time: true, 'no-dereference': false } },
230
+ operands: 'write*', flags: { r: 'read' }, stdin: false,
231
+ // -c: only existing files are touched (at runtime the path is known: check it)
232
+ when: {
233
+ 'c|no-create': {
234
+ operands: 'write~*',
235
+ extra: (r, { inv }) => ({ ...r, effects: r.effects.flatMap((e) => {
236
+ if (e.kind !== 'write' || !e.maybe)
237
+ return [e];
238
+ const p = concrete(e.path);
239
+ // statically, an earlier command of the snippet may create it
240
+ if (p === undefined || inv.mayHaveChanged?.(p))
241
+ return [e];
242
+ return fs.existsSync(p) ? [{ kind: 'write', path: e.path }] : [];
243
+ }) }),
244
+ },
245
+ },
246
+ }),
247
+ define({ names: ['mkdir'], summary: 'create directories', spec: { short: 'pv', shortWithArg: 'm', long: { parents: false, verbose: false, mode: '=m' } }, operands: 'write:dir*', stdin: false }),
248
+ define({ names: ['mkfifo', 'mknod'], summary: 'create special files', spec: { short: 'Z', shortWithArg: 'm', long: { mode: '=m' } }, operands: 'write arg*', stdin: false }),
249
+ define({ names: ['rmdir'], summary: 'remove empty directories', spec: { short: 'pv', long: { parents: false, 'ignore-fail-on-non-empty': false, verbose: false } }, operands: 'delete:dir*', stdin: false }),
250
+ define({
251
+ names: ['rm', 'unlink', 'shred'], summary: 'remove files',
252
+ spec: { short: 'rRfivdIuxzWP', shortWithArg: 'ns', long: { recursive: false, force: false, interactive: true, dir: false, verbose: false, 'one-file-system': false, 'no-preserve-root': false, 'preserve-root': false, iterations: '=n', size: '=s', remove: false, zero: false, exact: false } },
253
+ operands: 'delete*', recursive: ['r', 'R', 'recursive'], stdin: false,
254
+ extra: (r, { inv, p }) => {
255
+ // shred overwrites in place, and removes only with -u
256
+ if (inv.name === 'shred')
257
+ return { ...r, effects: r.effects.flatMap((e) => (e.kind === 'delete' ? [{ kind: 'write', path: e.path }, ...(p.flags.has('u') || p.flags.has('remove') ? [e] : [])] : [e])) };
258
+ // rm -f: a missing operand is not an error (for the opt-in `missingDeletes: 'allow'`)
259
+ if (inv.name === 'rm' && (p.flags.has('f') || p.flags.has('force')))
260
+ return { ...r, effects: r.effects.map((e) => (e.kind === 'delete' ? { ...e, ifExists: true } : e)) };
261
+ return r;
262
+ },
263
+ }),
264
+ {
265
+ names: ['cp', 'mv', 'install', 'ln', 'rsync'],
266
+ summary: 'copy / move / link files',
267
+ analyze(inv, h) {
268
+ const p = h.parseArgs(args(inv), {
269
+ short: 'aRrfipnvlsLPHdxubTuUcDzSWeXAEhOJN', shortWithArg: 'tmogSeBM',
270
+ long: { recursive: false, force: false, interactive: false, 'no-clobber': false, verbose: false, archive: false, symbolic: false, 'target-directory': '=t', 'no-target-directory': false, parents: false, preserve: true, 'no-preserve': true, update: false, backup: true, suffix: true, mode: '=m', owner: '=o', group: '=g', delete: false, 'delete-excluded': false, 'delete-after': false, 'delete-before': false, 'delete-during': false, exclude: true, include: true, 'exclude-from': true, 'include-from': true, 'files-from': true, filter: true, 'dry-run': false, progress: false, compress: false, 'remove-source-files': false, rsh: '=e', 'itemize-changes': false, 'ignore-existing': false, partial: false, 'partial-dir': true, stats: false, info: true, 'human-readable': false, checksum: false, times: false, perms: false, links: false, 'copy-links': false, 'hard-links': false, 'no-dereference': false, dereference: false, 'one-file-system': false, quiet: false, 'size-only': false, existing: false, inplace: false, append: false, 'append-verify': false, 'whole-file': false, 'chmod': true, 'chown': true, 'numeric-ids': false, 'mkpath': false, 'strip-trailing-slashes': false, reflink: true, sparse: true, 'copy-contents': false, 'attributes-only': false, 'relative': false, 'no-relative': false, 'safe-links': false, 'max-size': true, 'min-size': true, 'bwlimit': true, 'timeout': true, 'out-format': true, 'log-file': true, 'link-dest': true, 'compare-dest': true, 'copy-dest': true, 'backup-dir': true, 'temp-dir': true, 'no-motd': false, 'list-only': false, 'prune-empty-dirs': false, 'protect-args': false, 'exclude-dir': true }
271
+ });
272
+ // install -d DIR...: creates every operand
273
+ if (inv.name === 'install' && p.flags.has('d')) {
274
+ for (const d of p.operands)
275
+ h.role(d, 'dir');
276
+ return { effects: p.operands.map((d) => ({ kind: 'write', path: h.resolve(d) })), readsStdin: false, complete: !incomplete(p) };
277
+ }
278
+ // rsync SRC (no destination) lists SRC
279
+ if (inv.name === 'rsync' && p.operands.length === 1 && !p.flags.has('t')) {
280
+ const src = p.operands[0];
281
+ const effects = [{ kind: 'read', path: h.resolve(src), recursive: true }];
282
+ if (/^[^/]*:/.test(knownPrefix(src)))
283
+ effects.push({ kind: 'network', detail: 'remote rsync' });
284
+ return { effects, readsStdin: false, complete: !incomplete(p) && !p.flags.has('e') };
285
+ }
286
+ const ops = [...p.operands];
287
+ const t = p.flags.get('t');
288
+ const target = t && t !== true ? t : ops.pop();
289
+ if (!target)
290
+ return { effects: [], readsStdin: false, complete: !incomplete(p) };
291
+ const recursive = inv.name === 'rsync' || p.flags.has('r') || p.flags.has('R') || p.flags.has('a') || p.flags.has('recursive') || inv.name === 'mv';
292
+ const effects = [];
293
+ const dryRun = p.flags.has('dry-run') || (inv.name === 'rsync' && (p.flags.has('n') || p.flags.has('list-only')));
294
+ for (const src of ops) {
295
+ effects.push({ kind: 'read', path: h.resolve(src), recursive });
296
+ if (inv.name === 'mv' || p.flags.has('remove-source-files'))
297
+ effects.push({ kind: 'delete', path: h.resolve(src), recursive });
298
+ }
299
+ if (!dryRun)
300
+ effects.push({ kind: 'write', path: h.resolve(target), recursive });
301
+ let complete = !incomplete(p);
302
+ if (inv.name === 'rsync') {
303
+ const remote = (v) => /^[^/]*:/.test(knownPrefix(v));
304
+ if (ops.some(remote) || remote(target))
305
+ effects.push({ kind: 'network', detail: 'remote rsync' });
306
+ for (const k of ['exclude-from', 'include-from', 'files-from']) {
307
+ const v = p.flags.get(k);
308
+ if (v && v !== true && !isStdinOperand(v))
309
+ effects.push({ kind: 'read', path: h.resolve(v) });
310
+ }
311
+ const log = p.flags.get('log-file');
312
+ if (log && log !== true)
313
+ effects.push({ kind: 'write', path: h.resolve(log), append: true });
314
+ // the remote shell runs locally
315
+ if (p.flags.has('e'))
316
+ complete = false;
317
+ }
318
+ return { effects, readsStdin: false, complete };
319
+ },
320
+ },
321
+ {
322
+ names: ['chmod', 'chown', 'chgrp', 'chflags'],
323
+ summary: 'change file metadata',
324
+ analyze(inv, h) {
325
+ // `chmod -x file`: a mode that looks like a flag
326
+ const a = args(inv);
327
+ const m = inv.name === 'chmod' ? a.findIndex((v) => /^-[rwxXstugoa]+$/.test(concrete(v) ?? '') && !/^-[RfvchHLPN]+$/.test(concrete(v))) : -1;
328
+ return analyzeDef(CHMOD, inv, m === -1 ? a : [known('mode'), ...a.slice(0, m), ...a.slice(m + 1)], h);
329
+ },
330
+ },
331
+ // xattr [-lrsvx] [-p|-w|-d|-c] ...
332
+ define({
333
+ names: ['xattr'], summary: 'extended attributes',
334
+ spec: { short: 'lrsvxhpwdc' }, operands: 'read*', recursive: ['r'], stdin: false,
335
+ when: { p: { operands: 'arg read*' }, d: { operands: 'arg write*' }, c: { operands: 'write*' }, w: { operands: 'arg arg write*' } },
336
+ }),
337
+ define({
338
+ names: ['truncate'], summary: 'shrink or extend files',
339
+ spec: { short: 'co', shortWithArg: 'sr', long: { size: '=s', reference: '=r', 'no-create': false, 'io-blocks': false } },
340
+ operands: 'write*', flags: { r: 'read' }, stdin: false,
341
+ }),
342
+ {
343
+ names: ['patch'],
344
+ summary: 'apply a diff to files',
345
+ analyze(inv, h) {
346
+ const p = h.parseArgs(args(inv), {
347
+ short: 'bcefElnNRstTuvZ',
348
+ shortWithArg: 'pBdDFgioruVYzx',
349
+ long: {
350
+ strip: '=p', directory: '=d', input: '=i', output: '=o', 'reject-file': '=r', 'dry-run': false, forward: false, reverse: false,
351
+ silent: false, quiet: false, force: false, batch: false, unified: false, context: false, normal: false, backup: false,
352
+ 'remove-empty-files': false, binary: false, posix: false, suffix: '=z', prefix: '=B', 'version-control': '=V', fuzz: '=F', merge: true, verbose: false,
353
+ },
354
+ });
355
+ const flag = (k) => {
356
+ const v = p.flags.get(k);
357
+ return v && v !== true ? v : undefined;
358
+ };
359
+ const [original, patchfile] = p.operands;
360
+ const input = flag('i') ?? patchfile;
361
+ const output = flag('o');
362
+ const dir = flag('d');
363
+ const dry = p.flags.has('dry-run');
364
+ const effects = [];
365
+ if (input && !isStdinOperand(input))
366
+ effects.push({ kind: 'read', path: h.resolve(input) });
367
+ let complete = !incomplete(p);
368
+ if (original) {
369
+ const kind = dry || output ? 'read' : 'write';
370
+ if (dir === undefined)
371
+ effects.push({ kind, path: h.resolve(original) });
372
+ else {
373
+ // operands are relative to -d DIR
374
+ const d = concrete(h.resolve(dir));
375
+ const o = concrete(original);
376
+ if (d !== undefined && o !== undefined)
377
+ effects.push({ kind, path: h.resolve(known(o.startsWith('/') ? o : `${d}/${o}`)) });
378
+ else {
379
+ effects.push({ kind, path: h.resolve(dir), recursive: true });
380
+ complete = false;
381
+ }
382
+ }
383
+ }
384
+ else if (!dry && !output) {
385
+ // the files named in the diff, below the working (or -d) directory: GNU patch refuses
386
+ // absolute and `..` names; other implementations may not, so this is incomplete
387
+ effects.push({ kind: 'write', path: h.resolve(dir ?? known('.')), recursive: true });
388
+ complete = false;
389
+ }
390
+ if (output && !dry && !isStdinOperand(output))
391
+ effects.push({ kind: 'write', path: h.resolve(output) });
392
+ const rej = flag('r');
393
+ if (rej && !dry)
394
+ effects.push({ kind: 'write', path: h.resolve(rej) });
395
+ return { effects, readsStdin: !input || isStdinOperand(input), complete };
396
+ },
397
+ },
398
+ {
399
+ names: ['mktemp'],
400
+ summary: 'create a temporary file or directory',
401
+ analyze(inv, h) {
402
+ const p = h.parseArgs(args(inv), { short: 'dquturt', shortWithArg: 'p', long: { directory: false, quiet: false, 'dry-run': false, tmpdir: true, suffix: true } });
403
+ const tmp = (concrete(inv.env('TMPDIR') ?? known('/tmp')) ?? '/tmp').replace(/\/+$/, '') || '/tmp';
404
+ const pdir = p.flags.get('p') ?? p.flags.get('tmpdir');
405
+ const template = p.operands[0];
406
+ let under = tmp;
407
+ if (pdir && pdir !== true)
408
+ under = concrete(h.resolve(pdir));
409
+ else if (template) {
410
+ const t = concrete(template);
411
+ if (t === undefined)
412
+ under = undefined;
413
+ else if (t.includes('/'))
414
+ under = concrete(h.resolve(known(t.slice(0, t.lastIndexOf('/')) || '/')));
415
+ else if (!p.flags.has('t'))
416
+ under = concrete(inv.cwd);
417
+ }
418
+ const created = hole('path', '$(mktemp)', under ? { under } : {});
419
+ return {
420
+ effects: p.flags.has('u') || p.flags.has('dry-run') ? [] : [{ kind: 'write', path: created }],
421
+ readsStdin: false,
422
+ stdout: { type: 'path', under },
423
+ complete: !incomplete(p),
424
+ };
425
+ },
426
+ },
427
+ {
428
+ names: ['dd'],
429
+ summary: 'convert and copy',
430
+ analyze(inv, h) {
431
+ const effects = [];
432
+ let readsStdin = true;
433
+ for (const a of args(inv)) {
434
+ const s = knownPrefix(a);
435
+ const rest = (prefix) => ({ alts: a.alts.map((alt) => [typeof alt[0] === 'string' ? alt[0].slice(prefix.length) : '', ...alt.slice(1)]) });
436
+ if (s.startsWith('if=')) {
437
+ effects.push({ kind: 'read', path: h.resolve(rest('if=')) });
438
+ readsStdin = false;
439
+ }
440
+ else if (s.startsWith('of='))
441
+ effects.push({ kind: 'write', path: h.resolve(rest('of=')) });
442
+ }
443
+ return { effects, readsStdin };
444
+ },
445
+ },
446
+ {
447
+ names: ['sed', 'gsed'],
448
+ summary: 'stream editor',
449
+ analyze(inv, h) {
450
+ // -i: GNU takes an optional attached suffix (`-i.bak`, never the next word);
451
+ // BSD sed takes the next word as the suffix (`-i ''`, `-i .bak`)
452
+ const bsd = BSD && inv.name === 'sed';
453
+ const a = [];
454
+ const raw = args(inv);
455
+ for (let k = 0; k < raw.length; k++) {
456
+ const s = concrete(raw[k]);
457
+ const m = s === undefined ? null : /^-([nrEsuzl]*)i(.*)$/.exec(s);
458
+ if (!m) {
459
+ a.push(raw[k]);
460
+ if (s === '--')
461
+ a.push(...raw.slice(k + 1));
462
+ if (s === '--')
463
+ break;
464
+ continue;
465
+ }
466
+ a.push(known(`-${m[1]}i`));
467
+ if (m[2] === '' && bsd) {
468
+ const next = raw[k + 1] === undefined ? undefined : concrete(raw[k + 1]);
469
+ if (next !== undefined && (next === '' || /^[.~_-]/.test(next) && next !== '-'))
470
+ k++;
471
+ }
472
+ }
473
+ const p = h.parseArgs(a, { short: 'nrEsuzli', shortWithArg: 'efl', long: { expression: '=e', file: '=f', 'in-place': false, quiet: false, silent: false, 'regexp-extended': false, separate: false, 'null-data': false, 'zero-terminated': false, posix: false, sandbox: false, debug: false, 'line-length': '=l', 'follow-symlinks': false, unbuffered: false } });
474
+ const ops = [...p.operands];
475
+ const scripts = p.multi.get('e') ?? [];
476
+ if (scripts.length === 0 && !p.flags.has('f')) {
477
+ const s = ops.shift();
478
+ if (s)
479
+ scripts.push(s);
480
+ }
481
+ for (const s of scripts)
482
+ h.role(s, 'code');
483
+ const inPlace = p.flags.has('i') || p.flags.has('in-place');
484
+ const effects = ops.map((f) => ({ kind: 'read', path: h.resolve(f) }));
485
+ if (inPlace)
486
+ for (const f of ops)
487
+ effects.push({ kind: 'write', path: h.resolve(f) });
488
+ const sf = p.flags.get('f');
489
+ if (sf && sf !== true)
490
+ effects.push({ kind: 'read', path: h.resolve(sf) });
491
+ let complete = !incomplete(p);
492
+ for (const s of scripts) {
493
+ const texts = programTexts(s);
494
+ if (texts === undefined) {
495
+ complete = false;
496
+ continue;
497
+ }
498
+ for (const text of texts) {
499
+ // `w file` commands and the `e` command
500
+ const w = /(?:^|[;\n}])\s*(?:\d+|\/[^/]*\/|\$)?\s*[wW]\s+(\S+)/.exec(text) ?? /\/[^/]*\/[^/]*\/[gpIiMm0-9]*w\s+(\S+)/.exec(text);
501
+ if (w)
502
+ effects.push({ kind: 'write', path: h.resolve(known(w[1])) });
503
+ if (/(?:^|[;\n])\s*e(\s|$)/.test(text) || /\/[gpIiMm0-9]*e[gpIiMm0-9]*\s*(?:$|[;}\n])/.test(text)) {
504
+ effects.push({ kind: 'unknown', detail: 'sed e command executes shell commands' });
505
+ complete = false;
506
+ }
507
+ }
508
+ }
509
+ return { effects, readsStdin: ops.length === 0, complete };
510
+ },
511
+ },
512
+ {
513
+ names: ['awk', 'gawk', 'mawk', 'nawk'],
514
+ summary: 'pattern scanning language',
515
+ analyze(inv, h) {
516
+ const p = h.parseArgs(args(inv), { short: 'bcCdhlLnNOPrsStV', shortWithArg: 'FvfiE', long: { 'field-separator': '=F', assign: '=v', file: '=f' } });
517
+ const ops = [...p.operands];
518
+ let program;
519
+ const f = p.flags.get('f');
520
+ const effects = [];
521
+ if (f && f !== true)
522
+ effects.push({ kind: 'read', path: h.resolve(f) });
523
+ else
524
+ program = ops.shift();
525
+ if (program)
526
+ h.role(program, 'code');
527
+ const files = ops.filter((o) => !/^[A-Za-z_][A-Za-z0-9_]*=/.test(knownPrefix(o)));
528
+ for (const file of files)
529
+ effects.push({ kind: 'read', path: h.resolve(file) });
530
+ let complete = !incomplete(p) && program !== undefined;
531
+ for (const text of (program && programTexts(program)) || [undefined]) {
532
+ if (text === undefined) {
533
+ complete = false;
534
+ continue;
535
+ }
536
+ if (/\bsystem\s*\(|\|\s*"|"\s*\|\s*getline/.test(text)) {
537
+ effects.push({ kind: 'unknown', detail: 'awk program runs shell commands' });
538
+ complete = false;
539
+ }
540
+ for (const m of text.matchAll(/(?:print|printf)[^;}]*?>>?\s*"([^"]+)"/g))
541
+ effects.push({ kind: 'write', path: h.resolve(known(m[1])) });
542
+ if (/(?:print|printf)[^;}]*?>>?\s*[^"\s]/.test(text))
543
+ complete = false;
544
+ }
545
+ return { effects, readsStdin: files.length === 0, complete };
546
+ },
547
+ },
548
+ {
549
+ names: ['find'],
550
+ summary: 'search a directory tree',
551
+ analyze(inv, h) {
552
+ const a = args(inv);
553
+ const starts = [];
554
+ let i = 0;
555
+ while (i < a.length && /^-[HLP]$/.test(concrete(a[i]) ?? ''))
556
+ i++;
557
+ for (; i < a.length; i++) {
558
+ const s = concrete(a[i]);
559
+ if (s !== undefined && (s.startsWith('-') || s === '(' || s === '!' || s === ')'))
560
+ break;
561
+ starts.push(a[i]);
562
+ }
563
+ if (starts.length === 0)
564
+ starts.push(known('.'));
565
+ const roots = starts.map(h.resolve);
566
+ // find itself reads names and metadata; actions (-exec cat {}) report their own reads
567
+ const effects = roots.map((r) => ({ kind: 'read', path: r, recursive: true, names: true }));
568
+ let complete = true;
569
+ // a found path: lies under one of the start dirs
570
+ const foundPath = () => {
571
+ const r = roots.length === 1 ? concrete(roots[0]) : undefined;
572
+ return hole('path', 'find result', r ? { under: r } : {});
573
+ };
574
+ for (; i < a.length; i++) {
575
+ const s = concrete(a[i]);
576
+ if (s === undefined) {
577
+ complete = false;
578
+ continue;
579
+ }
580
+ // only matching files are deleted, and there may be none: a possible effect
581
+ if (s === '-delete')
582
+ for (const r of roots)
583
+ effects.push({ kind: 'delete', path: r, recursive: true, maybe: true });
584
+ else if (s === '-fprint' || s === '-fprint0' || s === '-fprintf' || s === '-fls') {
585
+ const f = a[++i];
586
+ if (f)
587
+ effects.push({ kind: 'write', path: h.resolve(f) });
588
+ if (s === '-fprintf')
589
+ i++;
590
+ }
591
+ else if (s === '-exec' || s === '-execdir' || s === '-ok' || s === '-okdir') {
592
+ // -execdir runs in the match's directory (somewhere under a start dir), with {} as ./NAME
593
+ const inDir = s.endsWith('dir');
594
+ const r = roots.length === 1 ? concrete(roots[0]) : undefined;
595
+ const cmd = [];
596
+ for (i++; i < a.length; i++) {
597
+ const t = concrete(a[i]);
598
+ if (t === ';' || t === '+')
599
+ break;
600
+ cmd.push(t === '{}' ? (inDir ? concat(known('./'), hole('filename', 'find result')) : foundPath()) : a[i]);
601
+ }
602
+ const sub = h.subcommand(cmd, { stdin: 'null', ...(inDir ? { cwd: hole('path', 'find result directory', r ? { under: r } : {}) } : {}) });
603
+ effects.push(...sub.effects.map(possibly));
604
+ if (sub.complete === false)
605
+ complete = false;
606
+ }
607
+ else if (['-name', '-iname', '-path', '-ipath', '-regex', '-iregex', '-wholename'].includes(s)) {
608
+ if (a[i + 1])
609
+ h.role(a[i + 1], 'pattern');
610
+ i++;
611
+ }
612
+ else if (['-name', '-iname', '-path', '-ipath', '-regex', '-iregex', '-type', '-maxdepth', '-mindepth', '-mtime', '-mmin', '-size', '-newer', '-user', '-group', '-perm', '-printf', '-wholename', '-samefile', '-atime', '-ctime', '-links', '-inum', '-uid', '-gid', '-cmin', '-amin', '-newermt', '-context', '-xtype', '-fstype'].includes(s)) {
613
+ i++;
614
+ }
615
+ }
616
+ return { effects, readsStdin: false, complete, stdout: { type: 'path', lines: true } };
617
+ },
618
+ },
619
+ {
620
+ names: ['xargs'],
621
+ summary: 'build command lines from stdin',
622
+ analyze(inv, h) {
623
+ // GNU -i[R], -e[EOF], -l[N] take optional attached arguments only
624
+ const a = [];
625
+ const raw = args(inv);
626
+ for (let k = 0; k < raw.length; k++) {
627
+ const s = concrete(raw[k]);
628
+ if (s === undefined || !s.startsWith('-') || s === '--') {
629
+ a.push(...raw.slice(k));
630
+ break;
631
+ }
632
+ const m = /^-([iel])(.*)$/.exec(s);
633
+ if (s === '--replace')
634
+ a.push(known('-I'), known('{}'));
635
+ else if (!m)
636
+ a.push(raw[k]);
637
+ else if (m[1] === 'i')
638
+ a.push(known('-I'), known(m[2] || '{}'));
639
+ else if (m[1] === 'l')
640
+ a.push(known('-L'), known(m[2] || '1'));
641
+ else if (m[2])
642
+ a.push(known('-E'), known(m[2]));
643
+ }
644
+ const p = h.parseArgs(a, { short: '0prtxo', shortWithArg: 'EILnPsdaJRS', long: { null: false, 'arg-file': '=a', delimiter: '=d', replace: '=I', 'max-args': '=n', 'max-procs': '=P', 'no-run-if-empty': false, verbose: false, interactive: false, exit: false, 'open-tty': false, 'max-lines': '=L', eof: '=E', 'max-chars': '=s', 'show-limits': false, 'process-slot-var': true }, posix: true });
645
+ const cmd = p.operands.length > 0 ? p.operands : [known('echo')];
646
+ const rep = p.flags.get('I') ?? p.flags.get('J');
647
+ const fromStdin = hole('string', 'xargs input');
648
+ const argv = rep && rep !== true
649
+ ? cmd.map((c) => (concrete(c) !== undefined && concrete(rep) !== undefined && concrete(c).includes(concrete(rep)) ? fromStdin : c))
650
+ : [...cmd, fromStdin];
651
+ const sub = h.subcommand(argv, { stdin: 'null' });
652
+ // runs once per input batch, possibly never (empty input)
653
+ const effects = sub.effects.map(possibly);
654
+ const af = p.flags.get('a');
655
+ if (af && af !== true)
656
+ effects.push({ kind: 'read', path: h.resolve(af) });
657
+ return { effects, readsStdin: !(af && af !== true), complete: sub.complete !== false && !incomplete(p) };
658
+ },
659
+ },
660
+ // ---- wrappers that run another command ----
661
+ {
662
+ names: ['env'],
663
+ summary: 'run a command in a modified environment',
664
+ analyze(inv, h) {
665
+ const a = args(inv);
666
+ let cwd = inv.cwd;
667
+ let i = 0;
668
+ for (; i < a.length; i++) {
669
+ const s = concrete(a[i]);
670
+ if (s === undefined)
671
+ break;
672
+ if (s === '-i' || s === '-' || s === '--ignore-environment' || s === '-0' || s === '--null' || s === '-v' || s === '--debug')
673
+ continue;
674
+ if (s === '-C' || s === '--chdir') {
675
+ if (a[i + 1])
676
+ cwd = h.resolve(a[i + 1]);
677
+ i++;
678
+ continue;
679
+ }
680
+ if (s.startsWith('--chdir=')) {
681
+ cwd = h.resolve(known(s.slice('--chdir='.length)));
682
+ continue;
683
+ }
684
+ if (s === '-u' || s === '-S' || s === '--unset' || s === '--split-string') {
685
+ // -S splits its argument into a command line: not modeled
686
+ if (s === '-S' || s === '--split-string')
687
+ return { effects: [{ kind: 'unknown', detail: 'env -S command line' }], readsStdin: 'maybe', complete: false };
688
+ i++;
689
+ continue;
690
+ }
691
+ if (/^--unset=/.test(s))
692
+ continue;
693
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(s))
694
+ continue;
695
+ break;
696
+ }
697
+ if (i >= a.length)
698
+ return { effects: [], readsStdin: false };
699
+ return h.subcommand(a.slice(i), { cwd });
700
+ },
701
+ },
702
+ ...['timeout', 'nice', 'nohup', 'time', 'stdbuf', 'ionice', 'caffeinate', 'chrt', 'taskset', 'unbuffer', 'command', 'builtin', 'exec'].map((name) => ({
703
+ names: [name],
704
+ summary: 'run another command',
705
+ analyze(inv, h) {
706
+ const a = args(inv);
707
+ let i = 0;
708
+ const withArg = { timeout: 'sk', nice: 'n', stdbuf: 'ioe', ionice: 'cnp', taskset: '', chrt: '', caffeinate: 'tw', command: '', builtin: '', exec: 'a', nohup: '', time: 'fo', unbuffer: '' };
709
+ for (; i < a.length; i++) {
710
+ const s = concrete(a[i]);
711
+ if (s === undefined || !s.startsWith('-') || s === '-')
712
+ break;
713
+ if (s === '--') {
714
+ i++;
715
+ break;
716
+ }
717
+ if (name === 'command' && (s.includes('v') || s.includes('V')))
718
+ return { effects: [], readsStdin: false, stdout: { type: 'path' } };
719
+ if (s.length === 2 && withArg[name].includes(s[1]))
720
+ i++;
721
+ }
722
+ if (name === 'timeout')
723
+ i++; // duration
724
+ if (name === 'taskset' || name === 'chrt')
725
+ i++;
726
+ const rest = a.slice(i);
727
+ if (rest.length === 0)
728
+ return { effects: [] };
729
+ return h.subcommand(rest);
730
+ },
731
+ })),
732
+ {
733
+ names: ['sudo', 'doas', 'su'],
734
+ summary: 'run a command as another user',
735
+ analyze(inv, h) {
736
+ const a = args(inv);
737
+ let i = 0;
738
+ for (; i < a.length; i++) {
739
+ const s = concrete(a[i]);
740
+ if (s === undefined || !s.startsWith('-'))
741
+ break;
742
+ if (['-u', '-g', '-C', '-D', '-h', '-p', '-r', '-t', '-U'].includes(s))
743
+ i++;
744
+ }
745
+ const sub = h.subcommand(a.slice(i));
746
+ return { ...sub, effects: [{ kind: 'process', detail: 'privilege escalation' }, { kind: 'unknown', detail: 'runs with elevated privileges outside the sandbox' }, ...sub.effects], complete: false };
747
+ },
748
+ },
749
+ {
750
+ names: ['bash', 'sh', 'zsh', 'dash', 'ksh'],
751
+ summary: 'run a shell',
752
+ analyze(inv, h) {
753
+ const a = args(inv);
754
+ let i = 0;
755
+ let script;
756
+ for (; i < a.length; i++) {
757
+ const s = concrete(a[i]);
758
+ if (s === undefined)
759
+ break;
760
+ if (s === '-c') {
761
+ script = a[++i];
762
+ i++;
763
+ break;
764
+ }
765
+ if (s === '-o' || s === '-O' || s === '+O' || s === '+o') {
766
+ i++;
767
+ continue;
768
+ }
769
+ if (/^-[a-zA-Z]+$/.test(s) && !s.includes('c'))
770
+ continue;
771
+ if (/^-[a-zA-Z]*c[a-zA-Z]*$/.test(s)) {
772
+ script = a[++i];
773
+ i++;
774
+ break;
775
+ }
776
+ break;
777
+ }
778
+ if (script)
779
+ return { ...h.script(script, a.slice(i)), readsStdin: 'maybe' };
780
+ const file = a[i];
781
+ if (!file)
782
+ return { effects: [{ kind: 'unknown', detail: 'shell reads commands from stdin' }], readsStdin: true, complete: false };
783
+ const text = readScript(h.resolve(file));
784
+ if (text !== undefined) {
785
+ const r = h.script(known(text), a.slice(i + 1), { file: concrete(h.resolve(file)) });
786
+ return { ...r, effects: [{ kind: 'read', path: h.resolve(file) }, ...r.effects], readsStdin: 'maybe' };
787
+ }
788
+ return {
789
+ effects: [{ kind: 'read', path: h.resolve(file) }, { kind: 'unknown', detail: `runs script ${concrete(file) ?? 'file'}` }],
790
+ readsStdin: 'maybe',
791
+ complete: false,
792
+ };
793
+ },
794
+ },
795
+ // ---- network ----
796
+ {
797
+ names: ['curl'],
798
+ summary: 'transfer a URL',
799
+ analyze(inv, h) {
800
+ const p = h.parseArgs(args(inv), {
801
+ short: 'sSfLIkvOJgiNqZ#lRB0123456', shortWithArg: 'oXHdAuFeTxwmrbcEKYyzCQDtP',
802
+ long: { output: '=o', 'remote-name': false, request: '=X', header: '=H', data: '=d', 'data-raw': true, 'data-binary': true, 'data-urlencode': true, form: '=F', 'upload-file': '=T', user: '=u', silent: false, 'show-error': false, fail: false, location: false, insecure: false, verbose: false, include: false, head: false, 'max-time': '=m', retry: true, 'connect-timeout': true, 'write-out': '=w', cookie: '=b', 'cookie-jar': '=c', 'dump-header': '=D', compressed: false, 'create-dirs': false, json: true, url: true, 'user-agent': '=A', proxy: '=x', 'fail-with-body': false, 'retry-delay': true, 'output-dir': true, 'remote-name-all': false, config: '=K' },
803
+ });
804
+ const effects = [];
805
+ const urls = [...p.operands, ...(p.multi.get('url') ?? [])];
806
+ for (const u of urls)
807
+ effects.push({ kind: 'network', target: u, detail: 'curl' });
808
+ for (const o of p.multi.get('o') ?? [])
809
+ if (concrete(o) !== '-')
810
+ effects.push({ kind: 'write', path: h.resolve(o) });
811
+ if (p.flags.has('O') || p.flags.has('remote-name'))
812
+ effects.push({ kind: 'write', path: h.resolve(hole('filename', 'remote name')) });
813
+ for (const k of ['c', 'D']) {
814
+ const v = p.flags.get(k);
815
+ if (v && v !== true && concrete(v) !== '-')
816
+ effects.push({ kind: 'write', path: h.resolve(v) });
817
+ }
818
+ for (const k of ['T', 'K']) {
819
+ const v = p.flags.get(k);
820
+ if (v && v !== true)
821
+ effects.push({ kind: 'read', path: h.resolve(v) });
822
+ }
823
+ for (const d of [...(p.multi.get('d') ?? []), ...(p.multi.get('data-binary') ?? []), ...(p.multi.get('F') ?? [])]) {
824
+ const s = knownPrefix(d);
825
+ if (s.startsWith('@'))
826
+ effects.push({ kind: 'read', path: h.resolve({ alts: d.alts.map((alt) => [typeof alt[0] === 'string' ? alt[0].slice(1) : '', ...alt.slice(1)]) }) });
827
+ const at = /^[^=]*=@/.exec(s);
828
+ if (at)
829
+ effects.push({ kind: 'read', path: h.resolve(known(s.slice(at[0].length))) });
830
+ }
831
+ return { effects, readsStdin: false, complete: !incomplete(p) };
832
+ },
833
+ },
834
+ {
835
+ names: ['wget'],
836
+ summary: 'download files',
837
+ analyze(inv, h) {
838
+ const p = h.parseArgs(args(inv), { short: 'qvcNrkpmb', shortWithArg: 'OPoaltTUe', long: { 'output-document': '=O', 'directory-prefix': '=P', quiet: false, 'no-verbose': false, continue: false, recursive: false, mirror: false, 'no-check-certificate': false, header: true, 'user-agent': '=U', timeout: '=T', tries: '=t' } });
839
+ const effects = p.operands.map((u) => ({ kind: 'network', target: u, detail: 'wget' }));
840
+ const out = p.flags.get('O');
841
+ const dir = p.flags.get('P');
842
+ if (out && out !== true) {
843
+ if (concrete(out) !== '-')
844
+ effects.push({ kind: 'write', path: h.resolve(out) });
845
+ }
846
+ else {
847
+ const base = dir && dir !== true ? h.resolve(dir) : inv.cwd;
848
+ effects.push({ kind: 'write', path: base, recursive: true });
849
+ }
850
+ return { effects, readsStdin: false, complete: !incomplete(p) };
851
+ },
852
+ },
853
+ ...Object.entries(NET_CLIENTS).map(([name, spec]) => ({
854
+ names: [name],
855
+ summary: 'network client',
856
+ analyze(inv, h) {
857
+ const p = h.parseArgs(args(inv), spec);
858
+ const remote = (v) => /^[^/]*:/.test(knownPrefix(v));
859
+ const ops = p.operands.filter((o) => !(name === 'dig' && knownPrefix(o).startsWith('+')));
860
+ // the host: dig @server, `host NAME [SERVER]`, `nslookup NAME [SERVER]`; scp's remote operands; else the first operand
861
+ const server = ops.find((o) => knownPrefix(o).startsWith('@'));
862
+ let target;
863
+ if (name === 'dig')
864
+ target = server ? { alts: server.alts.map((alt) => [typeof alt[0] === 'string' ? alt[0].slice(1) : alt[0], ...alt.slice(1)]) } : ops.find((o) => o !== server && !/^(A|AAAA|MX|NS|TXT|CNAME|SOA|PTR|SRV|ANY|CAA|HTTPS|IN|CH)$/.test(concrete(o) ?? ''));
865
+ else if (name === 'host' || name === 'nslookup')
866
+ target = ops[1] ?? ops[0];
867
+ else if (name === 'scp' || name === 'rsync')
868
+ target = ops.find(remote);
869
+ else
870
+ target = ops[0];
871
+ const effects = [{ kind: 'network', detail: name, ...(target ? { target } : {}) }];
872
+ // ssh keys and configuration
873
+ if (/^(ssh|scp|sftp)$/.test(name)) {
874
+ for (const k of ['i', 'F'])
875
+ for (const v of p.multi.get(k) ?? [])
876
+ effects.push({ kind: 'read', path: h.resolve(v) });
877
+ effects.push({ kind: 'read', path: concat(h.home() ?? known('~'), known('/.ssh')), recursive: true, maybe: true });
878
+ const log = p.flags.get('E');
879
+ if (log && log !== true)
880
+ effects.push({ kind: 'write', path: h.resolve(log), append: true });
881
+ }
882
+ if (name === 'scp') {
883
+ const files = p.operands;
884
+ for (const o of files.slice(0, -1))
885
+ if (!remote(o))
886
+ effects.push({ kind: 'read', path: h.resolve(o), ...(p.flags.has('r') ? { recursive: true } : {}) });
887
+ const last = files[files.length - 1];
888
+ if (last && !remote(last))
889
+ effects.push({ kind: 'write', path: h.resolve(last), ...(p.flags.has('r') ? { recursive: true } : {}) });
890
+ }
891
+ if (name === 'sftp')
892
+ for (const v of p.multi.get('b') ?? [])
893
+ if (concrete(v) !== '-')
894
+ effects.push({ kind: 'read', path: h.resolve(v) });
895
+ if (name === 'dig')
896
+ for (const k of ['f', 'k'])
897
+ for (const v of p.multi.get(k) ?? [])
898
+ effects.push({ kind: 'read', path: h.resolve(v) });
899
+ if (name === 'telnet')
900
+ for (const v of p.multi.get('n') ?? [])
901
+ effects.push({ kind: 'write', path: h.resolve(v) });
902
+ // queries are complete; sessions may run remote commands, transfer data, or (ProxyCommand, scp -S, nc -e) run local ones
903
+ const complete = /^(ping|ping6|dig|nslookup|host|traceroute|traceroute6)$/.test(name) && p.unknownFlags.length === 0;
904
+ if (!complete)
905
+ effects.push({ kind: 'unknown', detail: `${name} may run remote commands or transfer data` });
906
+ return { effects, readsStdin: complete ? false : 'maybe', complete };
907
+ },
908
+ })),
909
+ // ---- archives ----
910
+ {
911
+ names: ['tar', 'bsdtar', 'gtar'],
912
+ summary: 'archive files',
913
+ analyze(inv, h) {
914
+ const a = args(inv);
915
+ // first arg may be a bundled flag word without dash: `tar xzf x.tgz`
916
+ const first = concrete(a[0] ?? known(''));
917
+ const list = first !== undefined && /^[A-Za-z]+$/.test(first) && !first.startsWith('-') ? [known('-' + first), ...a.slice(1)] : a;
918
+ const p = h.parseArgs(list, { short: 'cxtrudzjJavpPkmhOSwBiG', shortWithArg: 'fCTXbIL', long: { create: false, extract: false, list: false, file: '=f', directory: '=C', gzip: false, bzip2: false, xz: false, zstd: false, 'auto-compress': false, verbose: false, exclude: true, 'exclude-from': '=X', 'files-from': '=T', null: false, 'no-xattrs': false, xattrs: false, 'keep-old-files': false, 'skip-old-files': false, overwrite: false, 'no-same-owner': false, 'no-same-permissions': false, 'strip-components': true, 'to-stdout': false, append: false, update: false, transform: true, 'exclude-vcs': false, 'owner': true, 'group': true, 'mtime': true, sort: true, 'numeric-owner': false, 'use-compress-program': '=I', wildcards: false, 'no-recursion': false, dereference: false, 'one-file-system': false, totals: false, 'checkpoint': true, 'warning': true, format: true, 'show-transformed-names': false, absolute: false } });
919
+ const archive = p.flags.get('f');
920
+ const dir = p.flags.get('C');
921
+ const base = dir && dir !== true ? h.resolve(dir) : inv.cwd;
922
+ const effects = [];
923
+ const resolveIn = (v) => (knownPrefix(v).startsWith('/') ? v : h.resolve(v));
924
+ const toStdin = !archive || archive === true || concrete(archive) === '-';
925
+ // member lists from a file (`-T list`, `-T -`) and compress programs: not modeled
926
+ const filesFrom = p.flags.get('T');
927
+ let complete = !incomplete(p) && !p.flags.has('I');
928
+ if (filesFrom && filesFrom !== true) {
929
+ complete = false;
930
+ if (concrete(filesFrom) !== '-')
931
+ effects.push({ kind: 'read', path: h.resolve(filesFrom) });
932
+ }
933
+ const excludeFrom = p.flags.get('X');
934
+ if (excludeFrom && excludeFrom !== true)
935
+ effects.push({ kind: 'read', path: h.resolve(excludeFrom) });
936
+ const listOnStdin = filesFrom !== undefined && filesFrom !== true && concrete(filesFrom) === '-';
937
+ if (p.flags.has('c') || p.flags.has('r') || p.flags.has('u') || p.flags.has('create')) {
938
+ for (const o of p.operands)
939
+ effects.push({ kind: 'read', path: dir && dir !== true ? concatPath(base, o) : resolveIn(o), recursive: true });
940
+ if (!toStdin)
941
+ effects.push({ kind: 'write', path: h.resolve(archive) });
942
+ return { effects, readsStdin: listOnStdin, complete };
943
+ }
944
+ if (!toStdin)
945
+ effects.push({ kind: 'read', path: h.resolve(archive) });
946
+ if (p.flags.has('x') || p.flags.has('extract')) {
947
+ if (!p.flags.has('O') && !p.flags.has('to-stdout'))
948
+ effects.push({ kind: 'write', path: base, recursive: true });
949
+ }
950
+ return { effects, readsStdin: toStdin || listOnStdin, complete };
951
+ },
952
+ },
953
+ {
954
+ names: ['unzip'],
955
+ summary: 'extract zip archives',
956
+ analyze(inv, h) {
957
+ const p = h.parseArgs(args(inv), { short: 'lnoqjpCLtvVXa', shortWithArg: 'dxP' });
958
+ const effects = [];
959
+ if (p.operands[0])
960
+ effects.push({ kind: 'read', path: h.resolve(p.operands[0]) });
961
+ const d = p.flags.get('d');
962
+ if (!p.flags.has('l') && !p.flags.has('p') && !p.flags.has('t'))
963
+ effects.push({ kind: 'write', path: d && d !== true ? h.resolve(d) : inv.cwd, recursive: true });
964
+ return { effects, readsStdin: false, complete: !incomplete(p) };
965
+ },
966
+ },
967
+ {
968
+ names: ['zip'],
969
+ summary: 'create zip archives',
970
+ analyze(inv, h) {
971
+ const p = h.parseArgs(args(inv), { short: 'rqvjmTyDXF0123456789', shortWithArg: 'xid' });
972
+ const [archive, ...files] = p.operands;
973
+ const effects = files.map((f) => ({ kind: 'read', path: h.resolve(f), recursive: p.flags.has('r') }));
974
+ if (archive)
975
+ effects.push({ kind: 'write', path: h.resolve(archive) });
976
+ if (p.flags.has('m'))
977
+ for (const f of files)
978
+ effects.push({ kind: 'delete', path: h.resolve(f), recursive: p.flags.has('r') });
979
+ return { effects, readsStdin: false, complete: !incomplete(p) };
980
+ },
981
+ },
982
+ {
983
+ names: ['gzip', 'gunzip', 'bzip2', 'bunzip2', 'xz', 'unxz', 'zstd', 'unzstd', 'zcat', 'bzcat', 'xzcat'],
984
+ summary: 'compress / decompress',
985
+ analyze(inv, h) {
986
+ const zstd = /zstd/.test(inv.name);
987
+ const p = h.parseArgs(args(inv), { short: 'cdfklnNqrtvzZ0123456789TeFhHLV', shortWithArg: zstd ? 'SoD' : 'S', long: { stdout: '=c', 'to-stdout': '=c', decompress: '=d', uncompress: '=d', keep: '=k', force: '=f', recursive: '=r', list: '=l', test: '=t', suffix: '=S', quiet: '=q', verbose: '=v', best: false, fast: false, rm: false, output: true, 'no-name': false, name: false, threads: true, ultra: false, long: false, rsyncable: false, extreme: false, format: true } });
988
+ const toStdout = p.flags.has('c') || /cat$/.test(inv.name) || p.flags.has('t') || p.flags.has('l');
989
+ const decompress = p.flags.has('d') || /^[gb]?un|cat$/.test(inv.name);
990
+ const recursive = p.flags.has('r');
991
+ const sv = p.flags.get('S');
992
+ const suffix = sv && sv !== true ? sv : known(/bz|bunzip/.test(inv.name) ? '.bz2' : inv.name.includes('xz') ? '.xz' : zstd ? '.zst' : '.gz');
993
+ // zstd keeps its input unless --rm; -o names the output
994
+ const keep = p.flags.has('k') || (zstd && !p.flags.has('rm'));
995
+ const out = p.flags.get('o') ?? p.flags.get('output');
996
+ const effects = [];
997
+ for (const f of p.operands) {
998
+ if (isStdinOperand(f))
999
+ continue;
1000
+ const src = h.resolve(f);
1001
+ effects.push({ kind: 'read', path: src, ...(recursive ? { recursive } : {}) });
1002
+ if (toStdout)
1003
+ continue;
1004
+ if (out && out !== true) {
1005
+ if (!isStdinOperand(out))
1006
+ effects.push({ kind: 'write', path: h.resolve(out) });
1007
+ }
1008
+ else if (recursive) {
1009
+ // every file below is replaced by its (de)compressed sibling
1010
+ effects.push({ kind: 'write', path: src, recursive: true });
1011
+ }
1012
+ else {
1013
+ // writes the (de)compressed sibling and removes the original
1014
+ const sfx = concrete(suffix);
1015
+ const name = concrete(src);
1016
+ const target = !decompress
1017
+ ? concat(src, suffix)
1018
+ : name !== undefined && sfx !== undefined && name.endsWith(sfx) && name.length > sfx.length
1019
+ ? known(name.slice(0, -sfx.length))
1020
+ : name !== undefined && /\.(t?gz|bz2|xz|zst|z|Z|tbz2?|txz|tzst)$/.test(name)
1021
+ ? known(name.replace(/\.(gz|bz2|xz|zst|z|Z)$/, '').replace(/\.t(gz|bz2?|xz|zst)$/, '.tar'))
1022
+ : name !== undefined ? concat(known(name.slice(0, name.lastIndexOf('/') + 1)), hole('filename', 'decompressed name')) : hole('path', 'decompressed file');
1023
+ effects.push({ kind: 'write', path: target });
1024
+ }
1025
+ if (!keep)
1026
+ effects.push({ kind: 'delete', path: src, ...(recursive ? { recursive } : {}) });
1027
+ }
1028
+ return { effects, readsStdin: p.operands.length === 0 || p.operands.some(isStdinOperand), complete: !incomplete(p) };
1029
+ },
1030
+ },
1031
+ // ---- process control ----
1032
+ {
1033
+ names: ['kill', 'pkill', 'killall'],
1034
+ summary: 'signal processes',
1035
+ analyze: (inv) => ({ effects: [{ kind: 'process', detail: `signal ${args(inv).map((a) => concrete(a) ?? '?').join(' ')}` }], readsStdin: false }),
1036
+ },
1037
+ ];
1038
+ /** Contents of a script file named by a concrete path (behaviors may read, never write). */
1039
+ export function readScript(p) {
1040
+ const file = concrete(p);
1041
+ if (file === undefined)
1042
+ return undefined;
1043
+ try {
1044
+ const st = fs.statSync(file);
1045
+ if (!st.isFile() || st.size > 1024 * 1024)
1046
+ return undefined;
1047
+ return fs.readFileSync(file, 'utf8');
1048
+ }
1049
+ catch {
1050
+ return undefined;
1051
+ }
1052
+ }
1053
+ /**
1054
+ * The possible texts of a program argument (sed script, awk program): each
1055
+ * alternative of a finite value, with integer holes (`$((a+1))`) filled in
1056
+ * by a digit, since a number cannot add a command. Undefined when the text
1057
+ * has other unknown parts.
1058
+ */
1059
+ function programTexts(v) {
1060
+ const out = [];
1061
+ for (const alt of v.alts) {
1062
+ let s = '';
1063
+ for (const seg of alt) {
1064
+ if (typeof seg === 'string')
1065
+ s += seg;
1066
+ else if (seg.type === 'int')
1067
+ s += '0';
1068
+ else
1069
+ return undefined;
1070
+ }
1071
+ out.push(s);
1072
+ }
1073
+ return out;
1074
+ }
1075
+ /** The same effect, marked as only possible. */
1076
+ function possibly(e) {
1077
+ return e.kind === 'read' || e.kind === 'write' || e.kind === 'delete' ? { ...e, maybe: true } : e;
1078
+ }
1079
+ function concatPath(base, rel) {
1080
+ if (knownPrefix(rel).startsWith('/'))
1081
+ return rel;
1082
+ return { alts: base.alts.flatMap((b) => rel.alts.map((r) => [...b, '/', ...r])) };
1083
+ }
1084
+ //# sourceMappingURL=core.js.map