@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.
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/mash.ts +253 -0
- package/dist/bin/mash.d.ts +39 -0
- package/dist/bin/mash.d.ts.map +1 -0
- package/dist/bin/mash.js +281 -0
- package/dist/bin/mash.js.map +1 -0
- package/dist/src/analysis/analyze.d.ts +97 -0
- package/dist/src/analysis/analyze.d.ts.map +1 -0
- package/dist/src/analysis/analyze.js +1406 -0
- package/dist/src/analysis/analyze.js.map +1 -0
- package/dist/src/analysis/fs.d.ts +39 -0
- package/dist/src/analysis/fs.d.ts.map +1 -0
- package/dist/src/analysis/fs.js +117 -0
- package/dist/src/analysis/fs.js.map +1 -0
- package/dist/src/analysis/outputs.d.ts +16 -0
- package/dist/src/analysis/outputs.d.ts.map +1 -0
- package/dist/src/analysis/outputs.js +277 -0
- package/dist/src/analysis/outputs.js.map +1 -0
- package/dist/src/analysis/value.d.ts +76 -0
- package/dist/src/analysis/value.d.ts.map +1 -0
- package/dist/src/analysis/value.js +248 -0
- package/dist/src/analysis/value.js.map +1 -0
- package/dist/src/behaviors/defs/core.d.ts +15 -0
- package/dist/src/behaviors/defs/core.d.ts.map +1 -0
- package/dist/src/behaviors/defs/core.js +1084 -0
- package/dist/src/behaviors/defs/core.js.map +1 -0
- package/dist/src/behaviors/defs/devtools.d.ts +9 -0
- package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/devtools.js +432 -0
- package/dist/src/behaviors/defs/devtools.js.map +1 -0
- package/dist/src/behaviors/defs/macos.d.ts +12 -0
- package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
- package/dist/src/behaviors/defs/macos.js +454 -0
- package/dist/src/behaviors/defs/macos.js.map +1 -0
- package/dist/src/behaviors/defs/tools.d.ts +9 -0
- package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/tools.js +639 -0
- package/dist/src/behaviors/defs/tools.js.map +1 -0
- package/dist/src/behaviors/dsl.d.ts +95 -0
- package/dist/src/behaviors/dsl.d.ts.map +1 -0
- package/dist/src/behaviors/dsl.js +267 -0
- package/dist/src/behaviors/dsl.js.map +1 -0
- package/dist/src/behaviors/index.d.ts +12 -0
- package/dist/src/behaviors/index.d.ts.map +1 -0
- package/dist/src/behaviors/index.js +18 -0
- package/dist/src/behaviors/index.js.map +1 -0
- package/dist/src/behaviors/registry.d.ts +45 -0
- package/dist/src/behaviors/registry.d.ts.map +1 -0
- package/dist/src/behaviors/registry.js +266 -0
- package/dist/src/behaviors/registry.js.map +1 -0
- package/dist/src/behaviors/roles.d.ts +29 -0
- package/dist/src/behaviors/roles.d.ts.map +1 -0
- package/dist/src/behaviors/roles.js +198 -0
- package/dist/src/behaviors/roles.js.map +1 -0
- package/dist/src/behaviors/types.d.ts +120 -0
- package/dist/src/behaviors/types.d.ts.map +1 -0
- package/dist/src/behaviors/types.js +12 -0
- package/dist/src/behaviors/types.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/deepsea.d.ts +121 -0
- package/dist/src/integrations/deepsea.d.ts.map +1 -0
- package/dist/src/integrations/deepsea.js +199 -0
- package/dist/src/integrations/deepsea.js.map +1 -0
- package/dist/src/machine.d.ts +139 -0
- package/dist/src/machine.d.ts.map +1 -0
- package/dist/src/machine.js +400 -0
- package/dist/src/machine.js.map +1 -0
- package/dist/src/parser/ast.d.ts +369 -0
- package/dist/src/parser/ast.d.ts.map +1 -0
- package/dist/src/parser/ast.js +17 -0
- package/dist/src/parser/ast.js.map +1 -0
- package/dist/src/parser/parser.d.ts +48 -0
- package/dist/src/parser/parser.d.ts.map +1 -0
- package/dist/src/parser/parser.js +1950 -0
- package/dist/src/parser/parser.js.map +1 -0
- package/dist/src/policy/effects.d.ts +86 -0
- package/dist/src/policy/effects.d.ts.map +1 -0
- package/dist/src/policy/effects.js +53 -0
- package/dist/src/policy/effects.js.map +1 -0
- package/dist/src/policy/policy.d.ts +130 -0
- package/dist/src/policy/policy.d.ts.map +1 -0
- package/dist/src/policy/policy.js +532 -0
- package/dist/src/policy/policy.js.map +1 -0
- package/dist/src/printer/doc.d.ts +151 -0
- package/dist/src/printer/doc.d.ts.map +1 -0
- package/dist/src/printer/doc.js +409 -0
- package/dist/src/printer/doc.js.map +1 -0
- package/dist/src/printer/highlight.d.ts +53 -0
- package/dist/src/printer/highlight.d.ts.map +1 -0
- package/dist/src/printer/highlight.js +339 -0
- package/dist/src/printer/highlight.js.map +1 -0
- package/dist/src/printer/html.d.ts +72 -0
- package/dist/src/printer/html.d.ts.map +1 -0
- package/dist/src/printer/html.js +423 -0
- package/dist/src/printer/html.js.map +1 -0
- package/dist/src/printer/print.d.ts +63 -0
- package/dist/src/printer/print.d.ts.map +1 -0
- package/dist/src/printer/print.js +484 -0
- package/dist/src/printer/print.js.map +1 -0
- package/dist/src/printer/summary.d.ts +50 -0
- package/dist/src/printer/summary.d.ts.map +1 -0
- package/dist/src/printer/summary.js +621 -0
- package/dist/src/printer/summary.js.map +1 -0
- package/dist/src/trace/events.d.ts +183 -0
- package/dist/src/trace/events.d.ts.map +1 -0
- package/dist/src/trace/events.js +9 -0
- package/dist/src/trace/events.js.map +1 -0
- package/dist/src/trace/progress.d.ts +58 -0
- package/dist/src/trace/progress.d.ts.map +1 -0
- package/dist/src/trace/progress.js +83 -0
- package/dist/src/trace/progress.js.map +1 -0
- package/dist/src/vm/arith.d.ts +29 -0
- package/dist/src/vm/arith.d.ts.map +1 -0
- package/dist/src/vm/arith.js +258 -0
- package/dist/src/vm/arith.js.map +1 -0
- package/dist/src/vm/builtins.d.ts +31 -0
- package/dist/src/vm/builtins.d.ts.map +1 -0
- package/dist/src/vm/builtins.js +1773 -0
- package/dist/src/vm/builtins.js.map +1 -0
- package/dist/src/vm/emulate.d.ts +75 -0
- package/dist/src/vm/emulate.d.ts.map +1 -0
- package/dist/src/vm/emulate.js +380 -0
- package/dist/src/vm/emulate.js.map +1 -0
- package/dist/src/vm/expand.d.ts +68 -0
- package/dist/src/vm/expand.d.ts.map +1 -0
- package/dist/src/vm/expand.js +744 -0
- package/dist/src/vm/expand.js.map +1 -0
- package/dist/src/vm/external.d.ts +34 -0
- package/dist/src/vm/external.d.ts.map +1 -0
- package/dist/src/vm/external.js +228 -0
- package/dist/src/vm/external.js.map +1 -0
- package/dist/src/vm/fifo.d.ts +66 -0
- package/dist/src/vm/fifo.d.ts.map +1 -0
- package/dist/src/vm/fifo.js +232 -0
- package/dist/src/vm/fifo.js.map +1 -0
- package/dist/src/vm/interp.d.ts +209 -0
- package/dist/src/vm/interp.d.ts.map +1 -0
- package/dist/src/vm/interp.js +1641 -0
- package/dist/src/vm/interp.js.map +1 -0
- package/dist/src/vm/io.d.ts +180 -0
- package/dist/src/vm/io.d.ts.map +1 -0
- package/dist/src/vm/io.js +502 -0
- package/dist/src/vm/io.js.map +1 -0
- package/dist/src/vm/pattern.d.ts +46 -0
- package/dist/src/vm/pattern.d.ts.map +1 -0
- package/dist/src/vm/pattern.js +498 -0
- package/dist/src/vm/pattern.js.map +1 -0
- package/dist/src/vm/proc.d.ts +94 -0
- package/dist/src/vm/proc.d.ts.map +1 -0
- package/dist/src/vm/proc.js +130 -0
- package/dist/src/vm/proc.js.map +1 -0
- package/dist/src/vm/state.d.ts +104 -0
- package/dist/src/vm/state.d.ts.map +1 -0
- package/dist/src/vm/state.js +233 -0
- package/dist/src/vm/state.js.map +1 -0
- package/dist/src/vm/test.d.ts +11 -0
- package/dist/src/vm/test.d.ts.map +1 -0
- package/dist/src/vm/test.js +277 -0
- package/dist/src/vm/test.js.map +1 -0
- package/package.json +63 -4
- package/src/analysis/analyze.ts +1395 -0
- package/src/analysis/fs.ts +121 -0
- package/src/analysis/outputs.ts +242 -0
- package/src/analysis/value.ts +264 -0
- package/src/behaviors/defs/core.ts +970 -0
- package/src/behaviors/defs/devtools.ts +400 -0
- package/src/behaviors/defs/macos.ts +426 -0
- package/src/behaviors/defs/tools.ts +596 -0
- package/src/behaviors/dsl.ts +293 -0
- package/src/behaviors/index.ts +19 -0
- package/src/behaviors/registry.ts +261 -0
- package/src/behaviors/roles.ts +187 -0
- package/src/behaviors/types.ts +127 -0
- package/src/index.ts +27 -0
- package/src/integrations/deepsea.ts +259 -0
- package/src/machine.ts +483 -0
- package/src/parser/ast.ts +472 -0
- package/src/parser/parser.ts +1826 -0
- package/src/policy/effects.ts +102 -0
- package/src/policy/policy.ts +578 -0
- package/src/printer/doc.ts +454 -0
- package/src/printer/highlight.ts +283 -0
- package/src/printer/html.ts +441 -0
- package/src/printer/print.ts +528 -0
- package/src/printer/summary.ts +601 -0
- package/src/trace/events.ts +102 -0
- package/src/trace/progress.ts +119 -0
- package/src/vm/arith.ts +236 -0
- package/src/vm/builtins.ts +1590 -0
- package/src/vm/emulate.ts +337 -0
- package/src/vm/expand.ts +721 -0
- package/src/vm/external.ts +265 -0
- package/src/vm/fifo.ts +226 -0
- package/src/vm/interp.ts +1623 -0
- package/src/vm/io.ts +570 -0
- package/src/vm/pattern.ts +477 -0
- package/src/vm/proc.ts +147 -0
- package/src/vm/state.ts +267 -0
- 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
|