@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,1590 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell builtins. Each receives a {@link BuiltinContext}: `proc` has the
|
|
3
|
+
* command's redirections applied; `base` is the shell process itself (same
|
|
4
|
+
* state, original fds) for builtins that change fds permanently (`exec`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs'
|
|
8
|
+
import * as os from 'node:os'
|
|
9
|
+
import * as path from 'node:path'
|
|
10
|
+
import type * as A from '../parser/ast.ts'
|
|
11
|
+
import { parse, ParseError } from '../parser/parser.ts'
|
|
12
|
+
import { known } from '../analysis/value.ts'
|
|
13
|
+
import { shellQuote, varElements } from './expand.ts'
|
|
14
|
+
import type { Interpreter } from './interp.ts'
|
|
15
|
+
import { resolveCommand } from './interp.ts'
|
|
16
|
+
import { AbortRun, CommandError, DiscardError, ExitSignal, LoopSignal, ReturnSignal, type Proc } from './proc.ts'
|
|
17
|
+
import { KNOWN_SHOPTS, newVar, SET_FLAGS, SET_O_NAMES, type ShellState, type Variable } from './state.ts'
|
|
18
|
+
import { evalTest } from './test.ts'
|
|
19
|
+
|
|
20
|
+
export interface BuiltinContext {
|
|
21
|
+
interp: Interpreter
|
|
22
|
+
proc: Proc
|
|
23
|
+
base: Proc
|
|
24
|
+
argv: string[]
|
|
25
|
+
node: A.SimpleCommand
|
|
26
|
+
/** If `arg` is a placeholder for an `Assign` node (declaration builtins), the node. */
|
|
27
|
+
assignArg(arg: string): A.Assign | null
|
|
28
|
+
out(text: string): Promise<void>
|
|
29
|
+
err(text: string): Promise<void>
|
|
30
|
+
error(message: string): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Builtin = (ctx: BuiltinContext) => Promise<number>
|
|
34
|
+
|
|
35
|
+
export const BUILTINS = new Map<string, Builtin>()
|
|
36
|
+
|
|
37
|
+
const def = (names: string[], fn: Builtin): void => {
|
|
38
|
+
for (const n of names) BUILTINS.set(n, fn)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Escapes
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/** Interpret backslash escapes (echo -e, printf %b). Returns [text, stop] where stop means \c was seen. */
|
|
46
|
+
export function interpretEscapes(s: string, printfStyle = false): [string, boolean] {
|
|
47
|
+
let out = ''
|
|
48
|
+
for (let i = 0; i < s.length; i++) {
|
|
49
|
+
const c = s[i]
|
|
50
|
+
if (c !== '\\' || i + 1 >= s.length) {
|
|
51
|
+
out += c
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
const e = s[++i]
|
|
55
|
+
switch (e) {
|
|
56
|
+
case 'a': out += '\x07'; break
|
|
57
|
+
case 'b': out += '\b'; break
|
|
58
|
+
case 'c': return [out, true]
|
|
59
|
+
case 'e': case 'E': out += '\x1b'; break
|
|
60
|
+
case 'f': out += '\f'; break
|
|
61
|
+
case 'n': out += '\n'; break
|
|
62
|
+
case 'r': out += '\r'; break
|
|
63
|
+
case 't': out += '\t'; break
|
|
64
|
+
case 'v': out += '\v'; break
|
|
65
|
+
case '\\': out += '\\'; break
|
|
66
|
+
case '"': out += printfStyle ? '"' : '\\"'; break
|
|
67
|
+
case "'": out += printfStyle ? "'" : "\\'"; break
|
|
68
|
+
case 'x': {
|
|
69
|
+
const m = /^[0-9a-fA-F]{1,2}/.exec(s.slice(i + 1))
|
|
70
|
+
if (!m) {
|
|
71
|
+
out += '\\x'
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
out += String.fromCharCode(parseInt(m[0], 16))
|
|
75
|
+
i += m[0].length
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
case 'u': case 'U': {
|
|
79
|
+
const m = new RegExp(`^[0-9a-fA-F]{1,${e === 'u' ? 4 : 8}}`).exec(s.slice(i + 1))
|
|
80
|
+
if (!m) {
|
|
81
|
+
out += '\\' + e
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
out += String.fromCodePoint(parseInt(m[0], 16))
|
|
85
|
+
i += m[0].length
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
case '0': {
|
|
89
|
+
const m = /^[0-7]{0,3}/.exec(s.slice(i + 1))!
|
|
90
|
+
out += String.fromCharCode(parseInt(m[0] || '0', 8) & 0xff)
|
|
91
|
+
i += m[0].length
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
if (printfStyle && /[1-7]/.test(e)) {
|
|
96
|
+
const m = /^[0-7]{0,2}/.exec(s.slice(i + 1))!
|
|
97
|
+
out += String.fromCharCode(parseInt(e + m[0], 8) & 0xff)
|
|
98
|
+
i += m[0].length
|
|
99
|
+
} else out += '\\' + e
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [out, false]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Trivial
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def([':', 'true'], async () => 0)
|
|
110
|
+
def(['false'], async () => 1)
|
|
111
|
+
|
|
112
|
+
def(['echo'], async (ctx) => {
|
|
113
|
+
const args = ctx.argv.slice(1)
|
|
114
|
+
let newline = true
|
|
115
|
+
let escapes = ctx.proc.state.shopts.has('xpg_echo')
|
|
116
|
+
while (args.length > 0 && /^-[neE]+$/.test(args[0])) {
|
|
117
|
+
for (const f of args[0].slice(1)) {
|
|
118
|
+
if (f === 'n') newline = false
|
|
119
|
+
else if (f === 'e') escapes = true
|
|
120
|
+
else escapes = false
|
|
121
|
+
}
|
|
122
|
+
args.shift()
|
|
123
|
+
}
|
|
124
|
+
let text = args.join(' ')
|
|
125
|
+
if (escapes) {
|
|
126
|
+
const [t, stop] = interpretEscapes(text)
|
|
127
|
+
if (stop) {
|
|
128
|
+
await ctx.out(t)
|
|
129
|
+
return 0
|
|
130
|
+
}
|
|
131
|
+
text = t
|
|
132
|
+
}
|
|
133
|
+
await ctx.out(newline ? text + '\n' : text)
|
|
134
|
+
return 0
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// printf
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function parseNumArg(arg: string | undefined, ctx: BuiltinContext, onError: () => void): bigint {
|
|
142
|
+
if (arg === undefined) return 0n
|
|
143
|
+
if (arg === '') {
|
|
144
|
+
// bash 5: an empty numeric argument is an invalid number (value 0, status 1); 3.2 accepted it
|
|
145
|
+
ctx.error(': invalid number')
|
|
146
|
+
onError()
|
|
147
|
+
return 0n
|
|
148
|
+
}
|
|
149
|
+
if (/^['"]/.test(arg)) return BigInt(arg.length > 1 ? arg.codePointAt(1)! : 0)
|
|
150
|
+
const t = arg.trim()
|
|
151
|
+
try {
|
|
152
|
+
if (/^[+-]?0[xX][0-9a-fA-F]+$/.test(t)) return BigInt(t.replace(/^([+-]?)0[xX]/, '$10x').replace(/^-0x(.*)$/, '-0x$1').startsWith('-') ? -BigInt(t.replace(/^-/, '')) : BigInt(t.replace(/^\+/, '')))
|
|
153
|
+
if (/^[+-]?0[0-7]+$/.test(t)) return (t.startsWith('-') ? -1n : 1n) * BigInt('0o' + t.replace(/^[+-]?0/, ''))
|
|
154
|
+
if (/^[+-]?\d+$/.test(t)) return BigInt(t)
|
|
155
|
+
} catch {
|
|
156
|
+
// fall through
|
|
157
|
+
}
|
|
158
|
+
const m = /^([+-]?\d+)/.exec(t)
|
|
159
|
+
ctx.error(`${arg}: invalid number`)
|
|
160
|
+
onError()
|
|
161
|
+
return m ? BigInt(m[1]) : 0n
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function formatPrintf(fmt: string, args: string[], ctx: BuiltinContext): { text: string; failed: boolean } {
|
|
165
|
+
let out = ''
|
|
166
|
+
let failed = false
|
|
167
|
+
let ai = 0
|
|
168
|
+
const next = (): string | undefined => args[ai++]
|
|
169
|
+
const fail = (): void => {
|
|
170
|
+
failed = true
|
|
171
|
+
}
|
|
172
|
+
let first = true
|
|
173
|
+
while (first || ai < args.length) {
|
|
174
|
+
first = false
|
|
175
|
+
const startAi = ai
|
|
176
|
+
for (let i = 0; i < fmt.length; i++) {
|
|
177
|
+
const c = fmt[i]
|
|
178
|
+
if (c === '\\') {
|
|
179
|
+
// escapes in the format
|
|
180
|
+
let j = i + 1
|
|
181
|
+
const e = fmt[j]
|
|
182
|
+
if (e === undefined) {
|
|
183
|
+
out += '\\'
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
let seq = '\\' + e
|
|
187
|
+
if (/[0-7]/.test(e)) {
|
|
188
|
+
const m = /^[0-7]{1,3}/.exec(fmt.slice(j))!
|
|
189
|
+
seq = '\\0' + m[0]
|
|
190
|
+
j += m[0].length - 1
|
|
191
|
+
} else if (e === 'x' || e === 'u' || e === 'U') {
|
|
192
|
+
const m = new RegExp(`^[0-9a-fA-F]{1,${e === 'x' ? 2 : e === 'u' ? 4 : 8}}`).exec(fmt.slice(j + 1))
|
|
193
|
+
if (m) {
|
|
194
|
+
seq += m[0]
|
|
195
|
+
j += m[0].length
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (seq === '\\c') {
|
|
199
|
+
out += '\\c'
|
|
200
|
+
i = j
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
const [t] = interpretEscapes(seq, true)
|
|
204
|
+
out += t
|
|
205
|
+
i = j
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
if (c !== '%') {
|
|
209
|
+
out += c
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
if (fmt[i + 1] === '%') {
|
|
213
|
+
out += '%'
|
|
214
|
+
i++
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
const m = /^%([-+ #0']*)(\*|\d+)?(?:\.(\*|\d*))?(?:hh|h|ll|l|L|q|j|z|t)?([diouxXfFeEgGcsbqaA])/.exec(fmt.slice(i))
|
|
218
|
+
if (!m) {
|
|
219
|
+
out += c
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
i += m[0].length - 1
|
|
223
|
+
const flags = m[1]
|
|
224
|
+
let width = m[2] === '*' ? Number(parseNumArg(next(), ctx, fail)) : m[2] ? Number(m[2]) : undefined
|
|
225
|
+
let leftAlign = flags.includes('-')
|
|
226
|
+
if (width !== undefined && width < 0) {
|
|
227
|
+
leftAlign = true
|
|
228
|
+
width = -width
|
|
229
|
+
}
|
|
230
|
+
const precision = m[3] === undefined ? undefined : m[3] === '*' ? Number(parseNumArg(next(), ctx, fail)) : Number(m[3] || '0')
|
|
231
|
+
const conv = m[4]
|
|
232
|
+
const isFloat = 'fFeEgGaA'.includes(conv)
|
|
233
|
+
const pad = (s: string, numeric: boolean): string => {
|
|
234
|
+
if (width === undefined || s.length >= width) return s
|
|
235
|
+
if (leftAlign) return s.padEnd(width)
|
|
236
|
+
if (numeric && flags.includes('0') && (precision === undefined || isFloat)) {
|
|
237
|
+
const sign = /^[+ -]/.test(s) ? s[0] : ''
|
|
238
|
+
return sign + s.slice(sign.length).padStart(width - sign.length, '0')
|
|
239
|
+
}
|
|
240
|
+
return s.padStart(width)
|
|
241
|
+
}
|
|
242
|
+
switch (conv) {
|
|
243
|
+
case 's': {
|
|
244
|
+
let s = next() ?? ''
|
|
245
|
+
if (precision !== undefined) s = s.slice(0, precision)
|
|
246
|
+
out += pad(s, false)
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
case 'b': {
|
|
250
|
+
const [t, stop] = interpretEscapes(next() ?? '', true)
|
|
251
|
+
let s = t
|
|
252
|
+
if (precision !== undefined) s = s.slice(0, precision)
|
|
253
|
+
out += pad(s, false)
|
|
254
|
+
if (stop) return { text: out, failed }
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
case 'q':
|
|
258
|
+
out += pad(printfQuote(next() ?? ''), false)
|
|
259
|
+
break
|
|
260
|
+
case 'c': {
|
|
261
|
+
// bash 5 prints the first byte, so an empty argument gives a NUL (3.2: nothing)
|
|
262
|
+
const a = next()
|
|
263
|
+
out += pad(a === '' ? '\0' : (a ?? '').slice(0, 1), false)
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
case 'd': case 'i': {
|
|
267
|
+
const n = parseNumArg(next(), ctx, fail)
|
|
268
|
+
let s = (n < 0n ? -n : n).toString()
|
|
269
|
+
if (precision !== undefined) s = s.padStart(precision, '0')
|
|
270
|
+
s = (n < 0n ? '-' : flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '') + s
|
|
271
|
+
out += pad(s, true)
|
|
272
|
+
break
|
|
273
|
+
}
|
|
274
|
+
case 'u': case 'o': case 'x': case 'X': {
|
|
275
|
+
let n = parseNumArg(next(), ctx, fail)
|
|
276
|
+
if (n < 0n) n = BigInt.asUintN(64, n)
|
|
277
|
+
let s = conv === 'o' ? n.toString(8) : conv === 'u' ? n.toString() : n.toString(16)
|
|
278
|
+
if (conv === 'X') s = s.toUpperCase()
|
|
279
|
+
if (precision !== undefined) s = s.padStart(precision, '0')
|
|
280
|
+
if (flags.includes('#') && n !== 0n) s = (conv === 'o' ? '0' : conv === 'x' ? '0x' : conv === 'X' ? '0X' : '') + s
|
|
281
|
+
out += pad(s, true)
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
default: {
|
|
285
|
+
// floating point
|
|
286
|
+
const raw = next()
|
|
287
|
+
let x = raw === undefined || raw === '' ? 0 : Number(raw)
|
|
288
|
+
if (Number.isNaN(x)) {
|
|
289
|
+
ctx.error(`${raw}: invalid number`)
|
|
290
|
+
fail()
|
|
291
|
+
x = 0
|
|
292
|
+
}
|
|
293
|
+
const p = precision ?? 6
|
|
294
|
+
let s: string
|
|
295
|
+
switch (conv) {
|
|
296
|
+
case 'f': case 'F': s = toFixedHalfEven(x, p); break
|
|
297
|
+
case 'e': case 'E': s = x.toExponential(p).replace(/e([+-])(\d)$/, 'e$10$2'); break
|
|
298
|
+
case 'g': case 'G': {
|
|
299
|
+
const pp = p === 0 ? 1 : p
|
|
300
|
+
const exp = x === 0 ? 0 : Math.floor(Math.log10(Math.abs(x)))
|
|
301
|
+
if (exp < -4 || exp >= pp) s = x.toExponential(pp - 1).replace(/\.?0+e/, 'e').replace(/e([+-])(\d)$/, 'e$10$2')
|
|
302
|
+
else s = String(Number(x.toPrecision(pp)))
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
default: s = x.toString(16)
|
|
306
|
+
}
|
|
307
|
+
if (conv === 'E' || conv === 'G' || conv === 'F') s = s.toUpperCase()
|
|
308
|
+
if (x >= 0 && flags.includes('+')) s = '+' + s
|
|
309
|
+
else if (x >= 0 && flags.includes(' ')) s = ' ' + s
|
|
310
|
+
out += pad(s, true)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (ai === startAi) break // format consumed no arguments
|
|
315
|
+
}
|
|
316
|
+
return { text: out, failed }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** `printf %q`: backslash-escape shell metacharacters (bash style). */
|
|
320
|
+
function printfQuote(s: string): string {
|
|
321
|
+
if (s === '') return "''"
|
|
322
|
+
if (/[\x00-\x1f\x7f]/.test(s)) return shellQuote(s)
|
|
323
|
+
return s.replace(/[^A-Za-z0-9_@%+=:,./-]/g, (c) => '\\' + c)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** C printf `%.Nf`: round the exact binary value, ties to even. */
|
|
327
|
+
export function toFixedHalfEven(x: number, digits: number): string {
|
|
328
|
+
if (!Number.isFinite(x)) return String(x)
|
|
329
|
+
const neg = x < 0 || Object.is(x, -0)
|
|
330
|
+
// exact decimal expansion of the double
|
|
331
|
+
const [mant, exp] = (() => {
|
|
332
|
+
const buf = new DataView(new ArrayBuffer(8))
|
|
333
|
+
buf.setFloat64(0, Math.abs(x))
|
|
334
|
+
const bits = buf.getBigUint64(0)
|
|
335
|
+
const e = Number((bits >> 52n) & 0x7ffn)
|
|
336
|
+
const f = bits & ((1n << 52n) - 1n)
|
|
337
|
+
return e === 0 ? [f, -1074] : [f | (1n << 52n), e - 1075]
|
|
338
|
+
})()
|
|
339
|
+
// value = mant * 2^exp; scaled = value * 10^digits as a rational num/den
|
|
340
|
+
let num = mant * 10n ** BigInt(digits)
|
|
341
|
+
let den = 1n
|
|
342
|
+
if (exp >= 0) num <<= BigInt(exp)
|
|
343
|
+
else den <<= BigInt(-exp)
|
|
344
|
+
let q = num / den
|
|
345
|
+
const r = num % den
|
|
346
|
+
if (r * 2n > den || (r * 2n === den && (q & 1n) === 1n)) q += 1n
|
|
347
|
+
let s = q.toString().padStart(digits + 1, '0')
|
|
348
|
+
if (digits > 0) s = s.slice(0, -digits) + '.' + s.slice(-digits)
|
|
349
|
+
return (neg && q !== 0n ? '-' : neg && x !== 0 ? '-' : '') + s
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
def(['printf'], async (ctx) => {
|
|
353
|
+
const args = ctx.argv.slice(1)
|
|
354
|
+
let varName: string | null = null
|
|
355
|
+
if (args[0] === '-v') {
|
|
356
|
+
varName = args[1] ?? null
|
|
357
|
+
args.splice(0, 2)
|
|
358
|
+
}
|
|
359
|
+
if (args[0] === '--') args.shift()
|
|
360
|
+
if (args.length === 0) {
|
|
361
|
+
ctx.error('usage: printf [-v var] format [arguments]')
|
|
362
|
+
return 2
|
|
363
|
+
}
|
|
364
|
+
const { text, failed } = formatPrintf(args[0], args.slice(1), ctx)
|
|
365
|
+
const element = varName === null ? null : /^([A-Za-z_]\w*)\[(.+)\]$/.exec(varName)
|
|
366
|
+
if (element) await ctx.interp.assignElement(ctx.proc, element[1], element[2], text)
|
|
367
|
+
else if (varName !== null) ctx.interp.assignScalar(ctx.proc, varName, text)
|
|
368
|
+
else await ctx.out(text)
|
|
369
|
+
return failed ? 1 : 0
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
// Directories
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
|
|
376
|
+
function changeDir(ctx: BuiltinContext, target: string, physical: boolean): number {
|
|
377
|
+
const st = ctx.proc.state
|
|
378
|
+
let abs = path.resolve(st.cwd, target)
|
|
379
|
+
let stat: fs.Stats
|
|
380
|
+
try {
|
|
381
|
+
stat = fs.statSync(abs)
|
|
382
|
+
} catch {
|
|
383
|
+
ctx.error(`${target}: No such file or directory`)
|
|
384
|
+
return 1
|
|
385
|
+
}
|
|
386
|
+
if (!stat.isDirectory()) {
|
|
387
|
+
ctx.error(`${target}: Not a directory`)
|
|
388
|
+
return 1
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
fs.accessSync(abs, fs.constants.X_OK)
|
|
392
|
+
} catch {
|
|
393
|
+
ctx.error(`${target}: Permission denied`)
|
|
394
|
+
return 1
|
|
395
|
+
}
|
|
396
|
+
if (physical) abs = fs.realpathSync(abs)
|
|
397
|
+
const old = st.cwd
|
|
398
|
+
st.cwd = abs
|
|
399
|
+
ctx.interp.assignScalar(ctx.proc, 'OLDPWD', old)
|
|
400
|
+
ctx.interp.assignScalar(ctx.proc, 'PWD', abs)
|
|
401
|
+
return 0
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
def(['cd'], async (ctx) => {
|
|
405
|
+
const args = ctx.argv.slice(1)
|
|
406
|
+
let physical = false
|
|
407
|
+
while (args[0] === '-P' || args[0] === '-L' || args[0] === '--') {
|
|
408
|
+
if (args[0] === '-P') physical = true
|
|
409
|
+
if (args.shift() === '--') break
|
|
410
|
+
}
|
|
411
|
+
const st = ctx.proc.state
|
|
412
|
+
let target = args[0]
|
|
413
|
+
if (target === undefined) {
|
|
414
|
+
target = st.getString('HOME') ?? os.homedir()
|
|
415
|
+
} else if (target === '-') {
|
|
416
|
+
const old = st.getString('OLDPWD')
|
|
417
|
+
if (!old) {
|
|
418
|
+
ctx.error('OLDPWD not set')
|
|
419
|
+
return 1
|
|
420
|
+
}
|
|
421
|
+
target = old
|
|
422
|
+
const r = changeDir(ctx, target, physical)
|
|
423
|
+
if (r === 0) await ctx.out(st.cwd + '\n')
|
|
424
|
+
return r
|
|
425
|
+
}
|
|
426
|
+
return changeDir(ctx, target, physical)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
def(['pwd'], async (ctx) => {
|
|
430
|
+
const physical = ctx.argv.includes('-P')
|
|
431
|
+
await ctx.out((physical ? fs.realpathSync(ctx.proc.state.cwd) : ctx.proc.state.cwd) + '\n')
|
|
432
|
+
return 0
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
def(['pushd'], async (ctx) => {
|
|
436
|
+
const st = ctx.proc.state
|
|
437
|
+
const dir = ctx.argv[1]
|
|
438
|
+
if (dir === undefined) {
|
|
439
|
+
const top = st.dirStack.shift()
|
|
440
|
+
if (!top) {
|
|
441
|
+
ctx.error('no other directory')
|
|
442
|
+
return 1
|
|
443
|
+
}
|
|
444
|
+
const cur = st.cwd
|
|
445
|
+
const r = changeDir(ctx, top, false)
|
|
446
|
+
if (r === 0) st.dirStack.unshift(cur)
|
|
447
|
+
else st.dirStack.unshift(top)
|
|
448
|
+
} else {
|
|
449
|
+
const cur = st.cwd
|
|
450
|
+
const r = changeDir(ctx, dir, false)
|
|
451
|
+
if (r !== 0) return r
|
|
452
|
+
st.dirStack.unshift(cur)
|
|
453
|
+
}
|
|
454
|
+
await ctx.out([st.cwd, ...st.dirStack].join(' ') + '\n')
|
|
455
|
+
return 0
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
def(['popd'], async (ctx) => {
|
|
459
|
+
const st = ctx.proc.state
|
|
460
|
+
const top = st.dirStack.shift()
|
|
461
|
+
if (top === undefined) {
|
|
462
|
+
ctx.error('directory stack empty')
|
|
463
|
+
return 1
|
|
464
|
+
}
|
|
465
|
+
const r = changeDir(ctx, top, false)
|
|
466
|
+
if (r !== 0) return r
|
|
467
|
+
await ctx.out([st.cwd, ...st.dirStack].join(' ') + '\n')
|
|
468
|
+
return 0
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
def(['dirs'], async (ctx) => {
|
|
472
|
+
const st = ctx.proc.state
|
|
473
|
+
if (ctx.argv.includes('-c')) {
|
|
474
|
+
st.dirStack = []
|
|
475
|
+
return 0
|
|
476
|
+
}
|
|
477
|
+
await ctx.out([st.cwd, ...st.dirStack].join(' ') + '\n')
|
|
478
|
+
return 0
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Variables
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
|
|
485
|
+
function declLine(name: string, v: Variable): string {
|
|
486
|
+
let flags = ''
|
|
487
|
+
if (v.value?.kind === 'indexed' || v.declaredKind === 'indexed') flags += 'a'
|
|
488
|
+
if (v.value?.kind === 'assoc' || v.declaredKind === 'assoc') flags += 'A'
|
|
489
|
+
if (v.integer) flags += 'i'
|
|
490
|
+
if (v.lower) flags += 'l'
|
|
491
|
+
if (v.readonly) flags += 'r'
|
|
492
|
+
if (v.upper) flags += 'u'
|
|
493
|
+
if (v.exported) flags += 'x'
|
|
494
|
+
const f = flags === '' ? '--' : '-' + flags
|
|
495
|
+
if (v.value === null) return `declare ${f} ${name}`
|
|
496
|
+
if (v.value.kind === 'string') return `declare ${f} ${name}=${dq(v.value.value)}`
|
|
497
|
+
if (v.value.kind === 'indexed') {
|
|
498
|
+
const items = [...v.value.values.entries()].sort((a, b) => a[0] - b[0]).map(([k, x]) => `[${k}]=${dq(x)}`)
|
|
499
|
+
return `declare ${f} ${name}=(${items.join(' ')})`
|
|
500
|
+
}
|
|
501
|
+
const items = [...v.value.values.entries()].map(([k, x]) => `[${shellQuote(k)}]=${dq(x)}`)
|
|
502
|
+
return `declare ${f} ${name}=(${items.join(' ')} )`
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function dq(s: string): string {
|
|
506
|
+
return '"' + s.replace(/(["\\$`])/g, '\\$1') + '"'
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function isName(s: string): boolean {
|
|
510
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(s)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function declare(ctx: BuiltinContext, variant: 'declare' | 'local' | 'export' | 'readonly'): Promise<number> {
|
|
514
|
+
const st = ctx.proc.state
|
|
515
|
+
const args = ctx.argv.slice(1)
|
|
516
|
+
const on = new Set<string>()
|
|
517
|
+
const off = new Set<string>()
|
|
518
|
+
let i = 0
|
|
519
|
+
for (; i < args.length; i++) {
|
|
520
|
+
const a = args[i]
|
|
521
|
+
if (ctx.assignArg(a)) break
|
|
522
|
+
if (a === '--') {
|
|
523
|
+
i++
|
|
524
|
+
break
|
|
525
|
+
}
|
|
526
|
+
if (/^[-+][a-zA-Z]+$/.test(a)) {
|
|
527
|
+
for (const f of a.slice(1)) (a[0] === '-' ? on : off).add(f)
|
|
528
|
+
} else break
|
|
529
|
+
}
|
|
530
|
+
const names = args.slice(i)
|
|
531
|
+
// expand every assignment value before assigning any (bash semantics)
|
|
532
|
+
const frozen = new Map<string, A.Assign>()
|
|
533
|
+
for (const arg of names) {
|
|
534
|
+
const a = ctx.assignArg(arg)
|
|
535
|
+
if (a) frozen.set(arg, await ctx.interp.freezeAssign(a, ctx.proc))
|
|
536
|
+
}
|
|
537
|
+
if (variant === 'local' && st.locals.length === 0) {
|
|
538
|
+
ctx.error('can only be used in a function')
|
|
539
|
+
return 1
|
|
540
|
+
}
|
|
541
|
+
const inFunction = st.locals.length > 0
|
|
542
|
+
const local = variant === 'local' || ((variant === 'declare') && inFunction && !on.has('g'))
|
|
543
|
+
if (variant === 'export') on.add('x')
|
|
544
|
+
if (variant === 'readonly') on.add('r')
|
|
545
|
+
|
|
546
|
+
if ((on.has('f') || off.has('f')) && (variant === 'export' || on.has('x') || off.has('x'))) {
|
|
547
|
+
let status = 0
|
|
548
|
+
for (const n of names) {
|
|
549
|
+
const f = st.functions.get(n)
|
|
550
|
+
if (!f) {
|
|
551
|
+
ctx.error(`${n}: not a function`)
|
|
552
|
+
status = 1
|
|
553
|
+
continue
|
|
554
|
+
}
|
|
555
|
+
st.functions.set(n, { ...f, exported: !off.has('f') && !off.has('x') })
|
|
556
|
+
}
|
|
557
|
+
return status
|
|
558
|
+
}
|
|
559
|
+
if (on.has('f') || on.has('F')) {
|
|
560
|
+
for (const n of names.length > 0 ? names : [...st.functions.keys()].sort()) {
|
|
561
|
+
const f = st.functions.get(n)
|
|
562
|
+
if (!f) return 1
|
|
563
|
+
await ctx.out(on.has('F') ? (names.length > 0 ? `${n}\n` : `declare -f ${n}\n`) : `${n} () ${ctx.interp.rt.text(f.def.body)}\n`)
|
|
564
|
+
}
|
|
565
|
+
return 0
|
|
566
|
+
}
|
|
567
|
+
if (names.length === 0 || (on.has('p') && names.length === 0)) {
|
|
568
|
+
for (const n of st.visibleNames()) {
|
|
569
|
+
const v = st.lookup(n)!
|
|
570
|
+
if (variant === 'export' && !v.exported) continue
|
|
571
|
+
if (variant === 'readonly' && !v.readonly) continue
|
|
572
|
+
await ctx.out(variant === 'export' ? `declare -x ${n}${v.value?.kind === 'string' ? '=' + dq(v.value.value) : ''}\n` : declLine(n, v) + '\n')
|
|
573
|
+
}
|
|
574
|
+
return 0
|
|
575
|
+
}
|
|
576
|
+
if (on.has('p')) {
|
|
577
|
+
let status = 0
|
|
578
|
+
for (const n of names) {
|
|
579
|
+
const v = st.lookup(n)
|
|
580
|
+
if (!v) {
|
|
581
|
+
ctx.error(`${n}: not found`)
|
|
582
|
+
status = 1
|
|
583
|
+
continue
|
|
584
|
+
}
|
|
585
|
+
await ctx.out(declLine(n, v) + '\n')
|
|
586
|
+
}
|
|
587
|
+
return status
|
|
588
|
+
}
|
|
589
|
+
const kind = on.has('A') ? 'assoc' : on.has('a') ? 'indexed' : undefined
|
|
590
|
+
let status = 0
|
|
591
|
+
const applyAttrs = (v: Variable): void => {
|
|
592
|
+
if (on.has('x')) v.exported = true
|
|
593
|
+
if (off.has('x')) v.exported = false
|
|
594
|
+
if (on.has('i')) v.integer = true
|
|
595
|
+
if (off.has('i')) v.integer = false
|
|
596
|
+
if (on.has('l')) {
|
|
597
|
+
v.lower = true
|
|
598
|
+
v.upper = false
|
|
599
|
+
}
|
|
600
|
+
if (on.has('u')) {
|
|
601
|
+
v.upper = true
|
|
602
|
+
v.lower = false
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
for (const arg of names) {
|
|
606
|
+
const assign = frozen.get(arg) ?? null
|
|
607
|
+
if (assign) {
|
|
608
|
+
const v = local ? st.declareLocal(assign.name) : st.ensure(assign.name)
|
|
609
|
+
if (v.readonly) {
|
|
610
|
+
ctx.error(`${assign.name}: readonly variable`)
|
|
611
|
+
status = 1
|
|
612
|
+
continue
|
|
613
|
+
}
|
|
614
|
+
applyAttrs(v)
|
|
615
|
+
if (kind && v.value === null) v.declaredKind = kind
|
|
616
|
+
await ctx.interp.performAssign(assign, ctx.proc, { local, declareKind: kind })
|
|
617
|
+
if (on.has('r')) v.readonly = true
|
|
618
|
+
continue
|
|
619
|
+
}
|
|
620
|
+
const eq = arg.indexOf('=')
|
|
621
|
+
const name = eq === -1 ? arg : arg.slice(0, eq).replace(/\+$/, '')
|
|
622
|
+
if (!isName(name.replace(/\[.*\]$/, ''))) {
|
|
623
|
+
ctx.error(`\`${arg}': not a valid identifier`)
|
|
624
|
+
status = 1
|
|
625
|
+
continue
|
|
626
|
+
}
|
|
627
|
+
const v = local ? st.declareLocal(name) : (variant === 'declare' && on.has('g') ? globalVar(st, name) : st.ensure(name))
|
|
628
|
+
if (eq !== -1 && v.readonly) {
|
|
629
|
+
ctx.error(`${name}: readonly variable`)
|
|
630
|
+
status = 1
|
|
631
|
+
continue
|
|
632
|
+
}
|
|
633
|
+
applyAttrs(v)
|
|
634
|
+
if (kind) {
|
|
635
|
+
if (v.value === null) v.declaredKind = kind
|
|
636
|
+
else if (kind === 'indexed' && v.value.kind === 'string') v.value = { kind: 'indexed', values: new Map([[0, v.value.value]]) }
|
|
637
|
+
else if (kind === 'assoc' && v.value.kind !== 'assoc') v.value = { kind: 'assoc', values: new Map() }
|
|
638
|
+
}
|
|
639
|
+
if (eq !== -1) {
|
|
640
|
+
const append = arg[eq - 1] === '+'
|
|
641
|
+
const value = arg.slice(eq + 1)
|
|
642
|
+
const old = append ? st.getString(name) ?? '' : ''
|
|
643
|
+
ctx.interp.assignScalar(ctx.proc, name, old + value, { local })
|
|
644
|
+
}
|
|
645
|
+
if (on.has('r')) v.readonly = true
|
|
646
|
+
if (off.has('r') && v.readonly) {
|
|
647
|
+
ctx.error(`${name}: readonly variable`)
|
|
648
|
+
status = 1
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return status
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function globalVar(st: ShellState, name: string): Variable {
|
|
655
|
+
let v = st.globals.get(name)
|
|
656
|
+
if (!v) {
|
|
657
|
+
v = newVar(null)
|
|
658
|
+
st.globals.set(name, v)
|
|
659
|
+
}
|
|
660
|
+
return v
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
def(['declare', 'typeset'], (ctx) => declare(ctx, 'declare'))
|
|
664
|
+
def(['local'], (ctx) => declare(ctx, 'local'))
|
|
665
|
+
def(['export'], async (ctx) => {
|
|
666
|
+
if (ctx.argv[1] === '-n') {
|
|
667
|
+
for (const n of ctx.argv.slice(2)) {
|
|
668
|
+
const v = ctx.proc.state.lookup(n)
|
|
669
|
+
if (v) v.exported = false
|
|
670
|
+
}
|
|
671
|
+
return 0
|
|
672
|
+
}
|
|
673
|
+
return declare(ctx, 'export')
|
|
674
|
+
})
|
|
675
|
+
def(['readonly'], (ctx) => declare(ctx, 'readonly'))
|
|
676
|
+
|
|
677
|
+
def(['unset'], async (ctx) => {
|
|
678
|
+
const st = ctx.proc.state
|
|
679
|
+
let mode: 'v' | 'f' | 'auto' = 'auto'
|
|
680
|
+
let status = 0
|
|
681
|
+
for (const a of ctx.argv.slice(1)) {
|
|
682
|
+
if (a === '-v') {
|
|
683
|
+
mode = 'v'
|
|
684
|
+
continue
|
|
685
|
+
}
|
|
686
|
+
if (a === '-f') {
|
|
687
|
+
mode = 'f'
|
|
688
|
+
continue
|
|
689
|
+
}
|
|
690
|
+
if (a === '-n') continue
|
|
691
|
+
if (mode === 'f') {
|
|
692
|
+
st.functions.delete(a)
|
|
693
|
+
continue
|
|
694
|
+
}
|
|
695
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)\[(.*)\]$/.exec(a)
|
|
696
|
+
if (m && (m[2] === '@' || m[2] === '*')) {
|
|
697
|
+
st.unset(m[1])
|
|
698
|
+
continue
|
|
699
|
+
}
|
|
700
|
+
if (m) {
|
|
701
|
+
const v = st.lookup(m[1])
|
|
702
|
+
if (v?.value?.kind === 'indexed') {
|
|
703
|
+
const idx = Number(await ctx.interp.arithText(m[2], ctx.proc))
|
|
704
|
+
v.value.values.delete(idx)
|
|
705
|
+
} else if (v?.value?.kind === 'assoc') v.value.values.delete(m[2])
|
|
706
|
+
continue
|
|
707
|
+
}
|
|
708
|
+
if (!isName(a)) {
|
|
709
|
+
ctx.error(`\`${a}': not a valid identifier`)
|
|
710
|
+
status = 1
|
|
711
|
+
continue
|
|
712
|
+
}
|
|
713
|
+
if (st.lookup(a)) {
|
|
714
|
+
if (!st.unset(a)) {
|
|
715
|
+
ctx.error(`${a}: cannot unset: readonly variable`)
|
|
716
|
+
status = 1
|
|
717
|
+
}
|
|
718
|
+
} else if (mode === 'auto') st.functions.delete(a)
|
|
719
|
+
}
|
|
720
|
+
return status
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
def(['set'], async (ctx) => {
|
|
724
|
+
const st = ctx.proc.state
|
|
725
|
+
const args = ctx.argv.slice(1)
|
|
726
|
+
if (args.length === 0) {
|
|
727
|
+
for (const n of st.visibleNames()) {
|
|
728
|
+
const v = st.lookup(n)!
|
|
729
|
+
if (v.value?.kind === 'string') await ctx.out(`${n}=${shellQuote(v.value.value)}\n`)
|
|
730
|
+
else if (v.value) await ctx.out(`${n}=(${varElements(v).map(shellQuote).join(' ')})\n`)
|
|
731
|
+
}
|
|
732
|
+
return 0
|
|
733
|
+
}
|
|
734
|
+
let i = 0
|
|
735
|
+
for (; i < args.length; i++) {
|
|
736
|
+
const a = args[i]
|
|
737
|
+
if (a === '--') {
|
|
738
|
+
st.positional = args.slice(i + 1)
|
|
739
|
+
return 0
|
|
740
|
+
}
|
|
741
|
+
if (a === '-' ) {
|
|
742
|
+
st.options.xtrace = false
|
|
743
|
+
st.options.verbose = false
|
|
744
|
+
st.positional = args.slice(i + 1)
|
|
745
|
+
return 0
|
|
746
|
+
}
|
|
747
|
+
if (!/^[-+]/.test(a)) break
|
|
748
|
+
const enable = a[0] === '-'
|
|
749
|
+
for (let j = 1; j < a.length; j++) {
|
|
750
|
+
const f = a[j]
|
|
751
|
+
if (f === 'o') {
|
|
752
|
+
const name = args[i + 1]
|
|
753
|
+
if (name === undefined || /^[-+]/.test(name)) {
|
|
754
|
+
for (const [n, opt] of Object.entries(SET_O_NAMES)) await ctx.out(`${n.padEnd(15)}\t${st.options[opt] ? 'on' : 'off'}\n`)
|
|
755
|
+
continue
|
|
756
|
+
}
|
|
757
|
+
i++
|
|
758
|
+
const opt = SET_O_NAMES[name]
|
|
759
|
+
if (!opt) {
|
|
760
|
+
if (['posix', 'emacs', 'vi', 'monitor', 'notify', 'hashall', 'braceexpand', 'histexpand', 'history', 'ignoreeof', 'interactive-comments', 'keyword', 'onecmd', 'physical', 'privileged', 'errtrace', 'functrace'].includes(name)) continue
|
|
761
|
+
ctx.error(`${name}: invalid option name`)
|
|
762
|
+
return 2
|
|
763
|
+
}
|
|
764
|
+
st.options[opt] = enable
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
const opt = SET_FLAGS[f]
|
|
768
|
+
if (opt) st.options[opt] = enable
|
|
769
|
+
else if (!'hBmbHPTkt'.includes(f)) {
|
|
770
|
+
ctx.error(`${a[0]}${f}: invalid option`)
|
|
771
|
+
return 2
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (i < args.length) st.positional = args.slice(i)
|
|
776
|
+
return 0
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
def(['shopt'], async (ctx) => {
|
|
780
|
+
const st = ctx.proc.state
|
|
781
|
+
const args = ctx.argv.slice(1)
|
|
782
|
+
let mode: 's' | 'u' | 'print' = 'print'
|
|
783
|
+
let quiet = false
|
|
784
|
+
let printMode = false
|
|
785
|
+
const names: string[] = []
|
|
786
|
+
for (const a of args) {
|
|
787
|
+
if (a === '-s') mode = 's'
|
|
788
|
+
else if (a === '-u') mode = 'u'
|
|
789
|
+
else if (a === '-q') quiet = true
|
|
790
|
+
else if (a === '-p') printMode = true
|
|
791
|
+
else if (a === '-o') continue
|
|
792
|
+
else names.push(a)
|
|
793
|
+
}
|
|
794
|
+
let status = 0
|
|
795
|
+
for (const n of names) {
|
|
796
|
+
if (!(KNOWN_SHOPTS as readonly string[]).includes(n)) {
|
|
797
|
+
ctx.error(`${n}: invalid shell option name`)
|
|
798
|
+
status = 1
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (status !== 0) return status
|
|
802
|
+
if (mode === 's') for (const n of names) st.shopts.add(n)
|
|
803
|
+
else if (mode === 'u') for (const n of names) st.shopts.delete(n)
|
|
804
|
+
else {
|
|
805
|
+
const list = names.length > 0 ? names : [...KNOWN_SHOPTS]
|
|
806
|
+
for (const n of list) {
|
|
807
|
+
const on = st.shopts.has(n)
|
|
808
|
+
if (!on) status = 1
|
|
809
|
+
if (!quiet) await ctx.out(printMode ? `shopt ${on ? '-s' : '-u'} ${n}\n` : `${n.padEnd(20)}\t${on ? 'on' : 'off'}\n`) // bash 5 width (3.2: 15)
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return status
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
def(['shift'], async (ctx) => {
|
|
816
|
+
const n = ctx.argv[1] === undefined ? 1 : Number(ctx.argv[1])
|
|
817
|
+
const st = ctx.proc.state
|
|
818
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
819
|
+
ctx.error(`${ctx.argv[1]}: numeric argument required`)
|
|
820
|
+
return 1
|
|
821
|
+
}
|
|
822
|
+
if (n > st.positional.length) return 1
|
|
823
|
+
st.positional = st.positional.slice(n)
|
|
824
|
+
return 0
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
def(['let'], async (ctx) => {
|
|
828
|
+
if (ctx.argv.length < 2) {
|
|
829
|
+
ctx.error('expression expected')
|
|
830
|
+
return 1
|
|
831
|
+
}
|
|
832
|
+
let last = 0n
|
|
833
|
+
for (const e of ctx.argv.slice(1)) last = await ctx.interp.arithText(e, ctx.proc)
|
|
834
|
+
return last !== 0n ? 0 : 1
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
// ---------------------------------------------------------------------------
|
|
838
|
+
// read / mapfile
|
|
839
|
+
// ---------------------------------------------------------------------------
|
|
840
|
+
|
|
841
|
+
def(['read'], async (ctx) => {
|
|
842
|
+
const st = ctx.proc.state
|
|
843
|
+
const args = ctx.argv.slice(1)
|
|
844
|
+
let raw = false
|
|
845
|
+
let arrayName: string | null = null
|
|
846
|
+
let delim: number | null = 10
|
|
847
|
+
let nchars: number | null = null
|
|
848
|
+
let exactChars = false
|
|
849
|
+
let fd = 0
|
|
850
|
+
let timeout: number | null = null
|
|
851
|
+
let prompt: string | null = null
|
|
852
|
+
let i = 0
|
|
853
|
+
for (; i < args.length; i++) {
|
|
854
|
+
const a = args[i]
|
|
855
|
+
if (!a.startsWith('-') || a === '-') break
|
|
856
|
+
if (a === '--') {
|
|
857
|
+
i++
|
|
858
|
+
break
|
|
859
|
+
}
|
|
860
|
+
for (let j = 1; j < a.length; j++) {
|
|
861
|
+
const f = a[j]
|
|
862
|
+
const takeArg = (): string => {
|
|
863
|
+
const rest = a.slice(j + 1)
|
|
864
|
+
j = a.length
|
|
865
|
+
if (rest !== '') return rest
|
|
866
|
+
return args[++i] ?? ''
|
|
867
|
+
}
|
|
868
|
+
switch (f) {
|
|
869
|
+
case 'r': raw = true; break
|
|
870
|
+
case 's': case 'e': break
|
|
871
|
+
case 'a': arrayName = takeArg(); break
|
|
872
|
+
case 'd': {
|
|
873
|
+
const d = takeArg()
|
|
874
|
+
delim = d === '' ? 0 : d.charCodeAt(0)
|
|
875
|
+
break
|
|
876
|
+
}
|
|
877
|
+
case 'n': nchars = Number(takeArg()); break
|
|
878
|
+
case 'N': nchars = Number(takeArg()); exactChars = true; break
|
|
879
|
+
case 'u': fd = Number(takeArg()); break
|
|
880
|
+
case 't': timeout = Number(takeArg()); break
|
|
881
|
+
case 'p': prompt = takeArg(); break
|
|
882
|
+
case 'i': takeArg(); break
|
|
883
|
+
default:
|
|
884
|
+
ctx.error(`-${f}: invalid option`)
|
|
885
|
+
return 2
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const names = args.slice(i)
|
|
890
|
+
const input = ctx.proc.fds.get(fd)
|
|
891
|
+
if (!input || !input.readable) {
|
|
892
|
+
ctx.error(`${fd}: invalid file descriptor: Bad file descriptor`)
|
|
893
|
+
return 1
|
|
894
|
+
}
|
|
895
|
+
if (prompt !== null) await ctx.err(prompt)
|
|
896
|
+
// read bytes
|
|
897
|
+
let bytes: Uint8Array | null
|
|
898
|
+
const doRead = async (signal?: AbortSignal): Promise<Uint8Array | null> => {
|
|
899
|
+
if (nchars !== null) {
|
|
900
|
+
// read up to n chars (bytes; good enough for ASCII), stopping at delim unless -N
|
|
901
|
+
let acc = new Uint8Array(0)
|
|
902
|
+
while (acc.length < nchars) {
|
|
903
|
+
const chunk = await input.read(signal)
|
|
904
|
+
if (chunk === null) break
|
|
905
|
+
let take = Math.min(chunk.length, nchars - acc.length)
|
|
906
|
+
if (!exactChars && delim !== null) {
|
|
907
|
+
const idx = chunk.subarray(0, take).indexOf(delim)
|
|
908
|
+
if (idx >= 0) take = idx + 1
|
|
909
|
+
}
|
|
910
|
+
const merged = new Uint8Array(acc.length + take)
|
|
911
|
+
merged.set(acc)
|
|
912
|
+
merged.set(chunk.subarray(0, take), acc.length)
|
|
913
|
+
acc = merged
|
|
914
|
+
input.unread(chunk.subarray(take))
|
|
915
|
+
if (!exactChars && delim !== null && acc[acc.length - 1] === delim) break
|
|
916
|
+
}
|
|
917
|
+
return acc.length === 0 ? null : acc
|
|
918
|
+
}
|
|
919
|
+
let acc = new Uint8Array(0)
|
|
920
|
+
for (;;) {
|
|
921
|
+
const part = await input.readUntil(delim)
|
|
922
|
+
if (part === null) return acc.length === 0 ? null : acc
|
|
923
|
+
const merged = new Uint8Array(acc.length + part.length)
|
|
924
|
+
merged.set(acc)
|
|
925
|
+
merged.set(part, acc.length)
|
|
926
|
+
acc = merged
|
|
927
|
+
// backslash-newline continuation without -r
|
|
928
|
+
if (!raw && delim === 10 && acc.length >= 2 && acc[acc.length - 1] === 10 && acc[acc.length - 2] === 92) continue
|
|
929
|
+
return acc
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (timeout !== null) {
|
|
933
|
+
const ac = new AbortController()
|
|
934
|
+
const timer = setTimeout(() => ac.abort(), timeout * 1000)
|
|
935
|
+
bytes = await doRead(ac.signal)
|
|
936
|
+
clearTimeout(timer)
|
|
937
|
+
if (ac.signal.aborted && bytes === null) return 142
|
|
938
|
+
} else {
|
|
939
|
+
bytes = await doRead()
|
|
940
|
+
}
|
|
941
|
+
if (bytes === null) {
|
|
942
|
+
for (const n of names.length > 0 ? names : arrayName ? [] : ['REPLY']) ctx.interp.assignScalar(ctx.proc, n, '')
|
|
943
|
+
if (arrayName) st.ensure(arrayName).value = { kind: 'indexed', values: new Map() }
|
|
944
|
+
return 1
|
|
945
|
+
}
|
|
946
|
+
let text = new TextDecoder().decode(bytes)
|
|
947
|
+
let hitDelim = false
|
|
948
|
+
if (delim !== null && nchars === null && text.length > 0 && text.charCodeAt(text.length - 1) === delim) {
|
|
949
|
+
text = text.slice(0, -1)
|
|
950
|
+
hitDelim = true
|
|
951
|
+
} else if (delim !== null && nchars !== null && !exactChars && text.length > 0 && text.charCodeAt(text.length - 1) === delim) {
|
|
952
|
+
text = text.slice(0, -1)
|
|
953
|
+
hitDelim = true
|
|
954
|
+
}
|
|
955
|
+
// process backslashes (without -r): \x -> x, \<newline> removed; mark escaped chars so IFS does not split them
|
|
956
|
+
const chars: { c: string; esc: boolean }[] = []
|
|
957
|
+
for (let k = 0; k < text.length; k++) {
|
|
958
|
+
const c = text[k]
|
|
959
|
+
if (!raw && c === '\\' && k + 1 < text.length) {
|
|
960
|
+
k++
|
|
961
|
+
if (text[k] === '\n') continue
|
|
962
|
+
chars.push({ c: text[k], esc: true })
|
|
963
|
+
continue
|
|
964
|
+
}
|
|
965
|
+
if (!raw && c === '\\') continue
|
|
966
|
+
chars.push({ c, esc: false })
|
|
967
|
+
}
|
|
968
|
+
const ifs = st.getString('IFS') ?? ' \t\n'
|
|
969
|
+
const isWs = (c: string): boolean => ifs.includes(c) && (c === ' ' || c === '\t' || c === '\n')
|
|
970
|
+
const isIfs = (c: { c: string; esc: boolean }): boolean => !c.esc && ifs.includes(c.c)
|
|
971
|
+
if (names.length === 0 && !arrayName) {
|
|
972
|
+
ctx.interp.assignScalar(ctx.proc, 'REPLY', chars.map((c) => c.c).join(''))
|
|
973
|
+
return hitDelim || nchars !== null ? 0 : 1
|
|
974
|
+
}
|
|
975
|
+
// split into fields
|
|
976
|
+
const fields: string[] = []
|
|
977
|
+
let pos = 0
|
|
978
|
+
const skipWs = (): void => {
|
|
979
|
+
while (pos < chars.length && !chars[pos].esc && isWs(chars[pos].c)) pos++
|
|
980
|
+
}
|
|
981
|
+
skipWs()
|
|
982
|
+
const targetCount = arrayName ? Infinity : names.length
|
|
983
|
+
while (pos < chars.length && fields.length < targetCount - 1) {
|
|
984
|
+
let f = ''
|
|
985
|
+
while (pos < chars.length && !isIfs(chars[pos])) f += chars[pos++].c
|
|
986
|
+
fields.push(f)
|
|
987
|
+
// consume one delimiter (with surrounding whitespace)
|
|
988
|
+
skipWs()
|
|
989
|
+
if (pos < chars.length && isIfs(chars[pos]) && !isWs(chars[pos].c)) {
|
|
990
|
+
pos++
|
|
991
|
+
skipWs()
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (pos < chars.length) {
|
|
995
|
+
// remainder goes to the last name; trim trailing IFS whitespace
|
|
996
|
+
let end = chars.length
|
|
997
|
+
while (end > pos && !chars[end - 1].esc && isWs(chars[end - 1].c)) end--
|
|
998
|
+
// a single trailing non-whitespace IFS delimiter is dropped from the last field
|
|
999
|
+
if (end - 1 > pos && isIfs(chars[end - 1]) && !isWs(chars[end - 1].c) && !chars.slice(pos, end - 1).some(isIfs)) {
|
|
1000
|
+
end--
|
|
1001
|
+
while (end > pos && !chars[end - 1].esc && isWs(chars[end - 1].c)) end--
|
|
1002
|
+
}
|
|
1003
|
+
let rest = chars.slice(pos, end).map((c) => c.c).join('')
|
|
1004
|
+
if (arrayName) {
|
|
1005
|
+
// arrays: split remainder fully
|
|
1006
|
+
rest = rest
|
|
1007
|
+
}
|
|
1008
|
+
fields.push(rest)
|
|
1009
|
+
}
|
|
1010
|
+
if (arrayName) {
|
|
1011
|
+
const values = new Map<number, string>()
|
|
1012
|
+
fields.forEach((f, k) => values.set(k, f))
|
|
1013
|
+
const v = st.ensure(arrayName)
|
|
1014
|
+
v.value = { kind: 'indexed', values }
|
|
1015
|
+
} else {
|
|
1016
|
+
names.forEach((n, k) => ctx.interp.assignScalar(ctx.proc, n, fields[k] ?? ''))
|
|
1017
|
+
}
|
|
1018
|
+
return hitDelim || nchars !== null ? 0 : 1
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
def(['mapfile', 'readarray'], async (ctx) => {
|
|
1022
|
+
const args = ctx.argv.slice(1)
|
|
1023
|
+
let strip = false
|
|
1024
|
+
let count = Infinity
|
|
1025
|
+
let skip = 0
|
|
1026
|
+
let delim = 10
|
|
1027
|
+
let fd = 0
|
|
1028
|
+
let name = 'MAPFILE'
|
|
1029
|
+
for (let i = 0; i < args.length; i++) {
|
|
1030
|
+
const a = args[i]
|
|
1031
|
+
if (a === '-t') strip = true
|
|
1032
|
+
else if (a === '-n') count = Number(args[++i]) || Infinity
|
|
1033
|
+
else if (a === '-s') skip = Number(args[++i])
|
|
1034
|
+
else if (a === '-d') {
|
|
1035
|
+
const d = args[++i] ?? ''
|
|
1036
|
+
delim = d === '' ? 0 : d.charCodeAt(0)
|
|
1037
|
+
} else if (a === '-u') fd = Number(args[++i])
|
|
1038
|
+
else if (a === '-C' || a === '-c' || a === '-O') i++
|
|
1039
|
+
else name = a
|
|
1040
|
+
}
|
|
1041
|
+
const input = ctx.proc.fds.get(fd)
|
|
1042
|
+
const values = new Map<number, string>()
|
|
1043
|
+
if (input?.readable) {
|
|
1044
|
+
let n = 0
|
|
1045
|
+
for (;;) {
|
|
1046
|
+
if (values.size >= count) break
|
|
1047
|
+
const line = await input.readUntil(delim)
|
|
1048
|
+
if (line === null) break
|
|
1049
|
+
let s = new TextDecoder().decode(line)
|
|
1050
|
+
if (strip && s.length > 0 && s.charCodeAt(s.length - 1) === delim) s = s.slice(0, -1)
|
|
1051
|
+
if (n++ < skip) continue
|
|
1052
|
+
values.set(values.size, s)
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
const v = ctx.proc.state.ensure(name)
|
|
1056
|
+
v.value = { kind: 'indexed', values }
|
|
1057
|
+
return 0
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
// ---------------------------------------------------------------------------
|
|
1061
|
+
// test
|
|
1062
|
+
// ---------------------------------------------------------------------------
|
|
1063
|
+
|
|
1064
|
+
def(['test'], async (ctx) => evalTest(ctx.argv.slice(1), ctx.proc))
|
|
1065
|
+
def(['['], async (ctx) => {
|
|
1066
|
+
if (ctx.argv[ctx.argv.length - 1] !== ']') {
|
|
1067
|
+
ctx.error("missing `]'")
|
|
1068
|
+
return 2
|
|
1069
|
+
}
|
|
1070
|
+
return evalTest(ctx.argv.slice(1, -1), ctx.proc)
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
// ---------------------------------------------------------------------------
|
|
1074
|
+
// Control flow
|
|
1075
|
+
// ---------------------------------------------------------------------------
|
|
1076
|
+
|
|
1077
|
+
function numArg(ctx: BuiltinContext, s: string | undefined, dflt: number): number {
|
|
1078
|
+
if (s === undefined) return dflt
|
|
1079
|
+
if (!/^-?\d+$/.test(s.trim())) {
|
|
1080
|
+
ctx.error(`${s}: numeric argument required`)
|
|
1081
|
+
return 2
|
|
1082
|
+
}
|
|
1083
|
+
return Number(s) & 0xff
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
def(['exit'], async (ctx) => {
|
|
1087
|
+
throw new ExitSignal(numArg(ctx, ctx.argv[1], ctx.proc.state.lastStatus))
|
|
1088
|
+
})
|
|
1089
|
+
|
|
1090
|
+
def(['return'], async (ctx) => {
|
|
1091
|
+
const st = ctx.proc.state
|
|
1092
|
+
if (st.funcStack.length === 0 && !ctx.proc.sourcing) {
|
|
1093
|
+
ctx.error("can only `return' from a function or sourced script")
|
|
1094
|
+
return 2 // bash 5 (3.2: 1)
|
|
1095
|
+
}
|
|
1096
|
+
throw new ReturnSignal(numArg(ctx, ctx.argv[1], st.lastStatus))
|
|
1097
|
+
})
|
|
1098
|
+
|
|
1099
|
+
def(['break', 'continue'], async (ctx) => {
|
|
1100
|
+
const n = ctx.argv[1] === undefined ? 1 : Number(ctx.argv[1])
|
|
1101
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1102
|
+
ctx.error(`${ctx.argv[1]}: loop count out of range`)
|
|
1103
|
+
return 1
|
|
1104
|
+
}
|
|
1105
|
+
if (ctx.proc.loopDepth === 0) return 0
|
|
1106
|
+
throw new LoopSignal(ctx.argv[0] as 'break' | 'continue', Math.min(n, ctx.proc.loopDepth))
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
def(['eval'], async (ctx) => {
|
|
1110
|
+
const src = ctx.argv.slice(1).join(' ')
|
|
1111
|
+
if (src.trim() === '') return 0
|
|
1112
|
+
let script: A.Script
|
|
1113
|
+
try {
|
|
1114
|
+
script = ctx.interp.rt.parse(src, 'eval')
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
if (e instanceof ParseError) {
|
|
1117
|
+
ctx.error(e.message)
|
|
1118
|
+
return 2
|
|
1119
|
+
}
|
|
1120
|
+
throw e
|
|
1121
|
+
}
|
|
1122
|
+
try {
|
|
1123
|
+
return await ctx.interp.execList(script.body, ctx.proc)
|
|
1124
|
+
} catch (e) {
|
|
1125
|
+
if (e instanceof DiscardError) {
|
|
1126
|
+
ctx.error(e.message)
|
|
1127
|
+
return e.status
|
|
1128
|
+
}
|
|
1129
|
+
throw e
|
|
1130
|
+
}
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
def(['source', '.'], async (ctx) => {
|
|
1134
|
+
const file = ctx.argv[1]
|
|
1135
|
+
if (file === undefined) {
|
|
1136
|
+
ctx.error('filename argument required')
|
|
1137
|
+
return 2
|
|
1138
|
+
}
|
|
1139
|
+
const st = ctx.proc.state
|
|
1140
|
+
let resolved = path.resolve(st.cwd, file)
|
|
1141
|
+
if (!file.includes('/') && !fs.existsSync(resolved)) {
|
|
1142
|
+
for (const dir of (st.getString('PATH') ?? '').split(':')) {
|
|
1143
|
+
const p = path.resolve(st.cwd, dir || '.', file)
|
|
1144
|
+
if (fs.existsSync(p)) {
|
|
1145
|
+
resolved = p
|
|
1146
|
+
break
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const a = await ctx.interp.rt.assessRedirects([{ kind: 'read', path: known(resolved), source: 'source' }], ctx.node, ctx.proc)
|
|
1151
|
+
if (a.action === 'abort') throw new AbortRun(a.reason, a.status)
|
|
1152
|
+
if (a.action === 'deny') {
|
|
1153
|
+
ctx.error(a.reason)
|
|
1154
|
+
return a.status
|
|
1155
|
+
}
|
|
1156
|
+
let text: string
|
|
1157
|
+
try {
|
|
1158
|
+
text = fs.readFileSync(resolved, 'utf8')
|
|
1159
|
+
} catch {
|
|
1160
|
+
ctx.error(`${file}: No such file or directory`)
|
|
1161
|
+
return 1
|
|
1162
|
+
}
|
|
1163
|
+
let script: A.Script
|
|
1164
|
+
try {
|
|
1165
|
+
script = ctx.interp.rt.parse(text, resolved)
|
|
1166
|
+
} catch (e) {
|
|
1167
|
+
if (e instanceof ParseError) {
|
|
1168
|
+
ctx.error(`${file}: ${e.message}`)
|
|
1169
|
+
return 2
|
|
1170
|
+
}
|
|
1171
|
+
throw e
|
|
1172
|
+
}
|
|
1173
|
+
const saved = st.positional
|
|
1174
|
+
if (ctx.argv.length > 2) st.positional = ctx.argv.slice(2)
|
|
1175
|
+
const wasSourcing = ctx.proc.sourcing
|
|
1176
|
+
ctx.proc.sourcing = true
|
|
1177
|
+
const savedSource = st.globals.get('BASH_SOURCE')
|
|
1178
|
+
st.globals.set('BASH_SOURCE', newVar({ kind: 'indexed', values: new Map([[0, file], ...[...(savedSource?.value?.kind === 'indexed' ? savedSource.value.values.values() : [])].map((v, i) => [i + 1, v] as [number, string])]) }))
|
|
1179
|
+
try {
|
|
1180
|
+
return await ctx.interp.execList(script.body, ctx.proc)
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
if (e instanceof ReturnSignal) return e.status
|
|
1183
|
+
throw e
|
|
1184
|
+
} finally {
|
|
1185
|
+
if (savedSource) st.globals.set('BASH_SOURCE', savedSource)
|
|
1186
|
+
else st.globals.delete('BASH_SOURCE')
|
|
1187
|
+
ctx.proc.sourcing = wasSourcing
|
|
1188
|
+
if (ctx.argv.length > 2) st.positional = saved
|
|
1189
|
+
}
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
def(['exec'], async (ctx) => {
|
|
1193
|
+
const args = ctx.argv.slice(1)
|
|
1194
|
+
while (args[0] === '-c' || args[0] === '-l' || args[0] === '--') args.shift()
|
|
1195
|
+
if (args[0] === '-a') args.splice(0, 2)
|
|
1196
|
+
if (args.length === 0) {
|
|
1197
|
+
// make this command's redirections permanent
|
|
1198
|
+
const base = ctx.base.fds
|
|
1199
|
+
const mine = new Map(ctx.proc.fds.entries())
|
|
1200
|
+
for (const [fd] of base.entries()) if (!mine.has(fd)) base.close(fd)
|
|
1201
|
+
for (const [fd, file] of mine) if (base.get(fd) !== file) base.set(fd, file.retain())
|
|
1202
|
+
return 0
|
|
1203
|
+
}
|
|
1204
|
+
const status = await ctx.interp.runArgv(args, ctx.proc, ctx.node, { externalOnly: true })
|
|
1205
|
+
// the shell process was replaced: its EXIT trap never runs
|
|
1206
|
+
ctx.proc.state.traps.delete('EXIT')
|
|
1207
|
+
throw new ExitSignal(status)
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
def(['command'], async (ctx) => {
|
|
1211
|
+
const args = ctx.argv.slice(1)
|
|
1212
|
+
let describe: 'v' | 'V' | null = null
|
|
1213
|
+
while (args[0]?.startsWith('-') && args[0] !== '-') {
|
|
1214
|
+
const a = args.shift()!
|
|
1215
|
+
if (a === '--') break
|
|
1216
|
+
if (a.includes('v')) describe = 'v'
|
|
1217
|
+
if (a.includes('V')) describe = 'V'
|
|
1218
|
+
}
|
|
1219
|
+
if (args.length === 0) return 0
|
|
1220
|
+
if (describe) {
|
|
1221
|
+
let status = 0
|
|
1222
|
+
for (const n of args) {
|
|
1223
|
+
const d = describeCommand(n, ctx.proc.state)
|
|
1224
|
+
if (!d) {
|
|
1225
|
+
if (describe === 'V') ctx.error(`${n}: not found`)
|
|
1226
|
+
status = 1
|
|
1227
|
+
continue
|
|
1228
|
+
}
|
|
1229
|
+
await ctx.out(describe === 'v' ? (d.kind === 'file' ? d.path! : n) + '\n' : d.text + '\n')
|
|
1230
|
+
}
|
|
1231
|
+
return status
|
|
1232
|
+
}
|
|
1233
|
+
return ctx.interp.runArgv(args, ctx.proc, ctx.node, { skipFunctions: true })
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
def(['builtin'], async (ctx) => {
|
|
1237
|
+
const args = ctx.argv.slice(1)
|
|
1238
|
+
if (args.length === 0) return 0
|
|
1239
|
+
if (!BUILTINS.has(args[0])) {
|
|
1240
|
+
ctx.error(`${args[0]}: not a shell builtin`)
|
|
1241
|
+
return 1
|
|
1242
|
+
}
|
|
1243
|
+
return ctx.interp.runArgv(args, ctx.proc, ctx.node, { builtinOnly: true })
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
interface CommandDescription {
|
|
1247
|
+
kind: 'keyword' | 'function' | 'builtin' | 'file' | 'alias'
|
|
1248
|
+
path?: string
|
|
1249
|
+
text: string
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const KEYWORDS = new Set(['if', 'then', 'else', 'elif', 'fi', 'case', 'esac', 'for', 'select', 'while', 'until', 'do', 'done', 'in', 'function', 'time', '{', '}', '!', '[[', ']]'])
|
|
1253
|
+
|
|
1254
|
+
function describeCommand(name: string, st: ShellState): CommandDescription | null {
|
|
1255
|
+
if (st.aliases.has(name)) return { kind: 'alias', text: `${name} is aliased to \`${st.aliases.get(name)}'` }
|
|
1256
|
+
if (KEYWORDS.has(name)) return { kind: 'keyword', text: `${name} is a shell keyword` }
|
|
1257
|
+
if (st.functions.has(name)) return { kind: 'function', text: `${name} is a function` }
|
|
1258
|
+
if (BUILTINS.has(name)) return { kind: 'builtin', text: `${name} is a shell builtin` }
|
|
1259
|
+
const p = resolveCommand(name, st)
|
|
1260
|
+
if (p) return { kind: 'file', path: p, text: `${name} is ${p}` }
|
|
1261
|
+
return null
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
def(['type'], async (ctx) => {
|
|
1265
|
+
const args = ctx.argv.slice(1)
|
|
1266
|
+
let mode: 'normal' | 't' | 'p' | 'P' = 'normal'
|
|
1267
|
+
let all = false
|
|
1268
|
+
const names: string[] = []
|
|
1269
|
+
for (const a of args) {
|
|
1270
|
+
if (a === '-t') mode = 't'
|
|
1271
|
+
else if (a === '-p') mode = 'p'
|
|
1272
|
+
else if (a === '-P') mode = 'P'
|
|
1273
|
+
else if (a === '-a') all = true
|
|
1274
|
+
else if (a === '-f') continue
|
|
1275
|
+
else names.push(a)
|
|
1276
|
+
}
|
|
1277
|
+
let status = 0
|
|
1278
|
+
for (const n of names) {
|
|
1279
|
+
if (all && mode === 'normal') {
|
|
1280
|
+
const lines: string[] = []
|
|
1281
|
+
const d0 = describeCommand(n, ctx.proc.state)
|
|
1282
|
+
if (d0 && d0.kind !== 'file') lines.push(d0.text)
|
|
1283
|
+
for (const dir of (ctx.proc.state.getString('PATH') ?? '').split(':')) {
|
|
1284
|
+
const p = path.join(dir || '.', n)
|
|
1285
|
+
try {
|
|
1286
|
+
if (fs.statSync(p).isFile()) {
|
|
1287
|
+
fs.accessSync(p, fs.constants.X_OK)
|
|
1288
|
+
lines.push(`${n} is ${p}`)
|
|
1289
|
+
}
|
|
1290
|
+
} catch {
|
|
1291
|
+
// not here
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (lines.length === 0) {
|
|
1295
|
+
ctx.error(`${n}: not found`)
|
|
1296
|
+
status = 1
|
|
1297
|
+
}
|
|
1298
|
+
for (const l of [...new Set(lines)]) await ctx.out(l + '\n')
|
|
1299
|
+
continue
|
|
1300
|
+
}
|
|
1301
|
+
const d = mode === 'P' ? (() => {
|
|
1302
|
+
const p = resolveCommand(n, ctx.proc.state)
|
|
1303
|
+
return p ? { kind: 'file' as const, path: p, text: '' } : null
|
|
1304
|
+
})() : describeCommand(n, ctx.proc.state)
|
|
1305
|
+
if (!d) {
|
|
1306
|
+
if (mode === 'normal') ctx.error(`${n}: not found`)
|
|
1307
|
+
status = 1
|
|
1308
|
+
continue
|
|
1309
|
+
}
|
|
1310
|
+
if (mode === 't') await ctx.out(d.kind + '\n')
|
|
1311
|
+
else if (mode === 'p' || mode === 'P') {
|
|
1312
|
+
if (d.kind === 'file') await ctx.out(d.path + '\n')
|
|
1313
|
+
} else await ctx.out(d.text + '\n')
|
|
1314
|
+
}
|
|
1315
|
+
return status
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
def(['hash'], async (ctx) => {
|
|
1319
|
+
if (ctx.argv.includes('-r')) ctx.proc.state.hash.clear()
|
|
1320
|
+
if (ctx.argv[1] === '-t') {
|
|
1321
|
+
let status = 0
|
|
1322
|
+
for (const n of ctx.argv.slice(2)) {
|
|
1323
|
+
const p = ctx.proc.state.hash.get(n)
|
|
1324
|
+
if (p) await ctx.out(p + '\n')
|
|
1325
|
+
else {
|
|
1326
|
+
ctx.error(`${n}: not found`)
|
|
1327
|
+
status = 1
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return status
|
|
1331
|
+
}
|
|
1332
|
+
for (const n of ctx.argv.slice(1).filter((a) => !a.startsWith('-'))) {
|
|
1333
|
+
if (!resolveCommand(n, ctx.proc.state)) {
|
|
1334
|
+
ctx.error(`${n}: not found`)
|
|
1335
|
+
return 1
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return 0
|
|
1339
|
+
})
|
|
1340
|
+
|
|
1341
|
+
def(['alias'], async (ctx) => {
|
|
1342
|
+
const st = ctx.proc.state
|
|
1343
|
+
for (const a of ctx.argv.slice(1)) {
|
|
1344
|
+
const eq = a.indexOf('=')
|
|
1345
|
+
if (eq === -1) {
|
|
1346
|
+
const v = st.aliases.get(a)
|
|
1347
|
+
if (v === undefined) return 1
|
|
1348
|
+
await ctx.out(`alias ${a}=${shellQuote(v)}\n`)
|
|
1349
|
+
} else st.aliases.set(a.slice(0, eq), a.slice(eq + 1))
|
|
1350
|
+
}
|
|
1351
|
+
return 0
|
|
1352
|
+
})
|
|
1353
|
+
|
|
1354
|
+
def(['unalias'], async (ctx) => {
|
|
1355
|
+
for (const a of ctx.argv.slice(1)) {
|
|
1356
|
+
if (a === '-a') ctx.proc.state.aliases.clear()
|
|
1357
|
+
else ctx.proc.state.aliases.delete(a)
|
|
1358
|
+
}
|
|
1359
|
+
return 0
|
|
1360
|
+
})
|
|
1361
|
+
|
|
1362
|
+
// ---------------------------------------------------------------------------
|
|
1363
|
+
// Jobs, traps
|
|
1364
|
+
// ---------------------------------------------------------------------------
|
|
1365
|
+
|
|
1366
|
+
def(['wait'], async (ctx) => {
|
|
1367
|
+
const proc = ctx.proc
|
|
1368
|
+
const args = ctx.argv.slice(1).filter((a) => a !== '-f')
|
|
1369
|
+
if (args[0] === '-n') {
|
|
1370
|
+
const pending = [...proc.jobs.values()].filter((j) => j.status === null)
|
|
1371
|
+
if (pending.length === 0) return 127
|
|
1372
|
+
const s = await Promise.race(pending.map((j) => j.done))
|
|
1373
|
+
return s
|
|
1374
|
+
}
|
|
1375
|
+
if (args.length === 0) {
|
|
1376
|
+
for (const j of proc.jobs.values()) await j.done
|
|
1377
|
+
proc.jobs.clear()
|
|
1378
|
+
return 0
|
|
1379
|
+
}
|
|
1380
|
+
let status = 0
|
|
1381
|
+
for (const a of args) {
|
|
1382
|
+
const pid = Number(a.replace(/^%/, ''))
|
|
1383
|
+
const job = proc.jobs.get(pid) ?? [...proc.jobs.values()][pid - 1]
|
|
1384
|
+
if (!job) {
|
|
1385
|
+
ctx.error(`pid ${a} is not a child of this shell`)
|
|
1386
|
+
status = 127
|
|
1387
|
+
continue
|
|
1388
|
+
}
|
|
1389
|
+
status = await job.done
|
|
1390
|
+
proc.jobs.delete(job.pid)
|
|
1391
|
+
}
|
|
1392
|
+
return status
|
|
1393
|
+
})
|
|
1394
|
+
|
|
1395
|
+
def(['jobs'], async (ctx) => {
|
|
1396
|
+
let i = 1
|
|
1397
|
+
for (const j of ctx.proc.jobs.values()) {
|
|
1398
|
+
await ctx.out(ctx.argv.includes('-p') ? `${j.pid}\n` : `[${i++}] ${j.status === null ? 'Running' : 'Done'}\t${j.pid}\n`)
|
|
1399
|
+
}
|
|
1400
|
+
return 0
|
|
1401
|
+
})
|
|
1402
|
+
|
|
1403
|
+
def(['kill'], async (ctx) => {
|
|
1404
|
+
const args = ctx.argv.slice(1)
|
|
1405
|
+
let sig: NodeJS.Signals = 'SIGTERM'
|
|
1406
|
+
if (args[0] === '-l') {
|
|
1407
|
+
await ctx.out(Object.keys(os.constants.signals).map((s) => s.replace(/^SIG/, '')).join(' ') + '\n')
|
|
1408
|
+
return 0
|
|
1409
|
+
}
|
|
1410
|
+
if (args[0] === '-s' || args[0] === '-n') {
|
|
1411
|
+
args.shift()
|
|
1412
|
+
const s = args.shift() ?? 'TERM'
|
|
1413
|
+
sig = (/^\d+$/.test(s) ? Object.entries(os.constants.signals).find(([, n]) => n === Number(s))?.[0] : 'SIG' + s.replace(/^SIG/, '')) as NodeJS.Signals
|
|
1414
|
+
} else if (args[0]?.startsWith('-') && args[0] !== '--') {
|
|
1415
|
+
const s = args.shift()!.slice(1)
|
|
1416
|
+
if (s === '0') sig = 'SIG0' as NodeJS.Signals
|
|
1417
|
+
else sig = (/^\d+$/.test(s) ? Object.entries(os.constants.signals).find(([, n]) => n === Number(s))?.[0] ?? 'SIGTERM' : 'SIG' + s.replace(/^SIG/, '')) as NodeJS.Signals
|
|
1418
|
+
}
|
|
1419
|
+
if (args[0] === '--') args.shift()
|
|
1420
|
+
let status = 0
|
|
1421
|
+
for (const a of args) {
|
|
1422
|
+
const n = Number(a.replace(/^%/, ''))
|
|
1423
|
+
const job = ctx.proc.jobs.get(n) ?? (a.startsWith('%') ? [...ctx.proc.jobs.values()][n - 1] : undefined) ?? ctx.interp.rt.findJob(n)
|
|
1424
|
+
const signalZero = sig === undefined || (sig as string) === 'SIG0' || (sig as string) === 'SIGEXIT'
|
|
1425
|
+
if (job) {
|
|
1426
|
+
if (signalZero) {
|
|
1427
|
+
if (job.status !== null) status = 1
|
|
1428
|
+
continue
|
|
1429
|
+
}
|
|
1430
|
+
job.kill(sig)
|
|
1431
|
+
continue
|
|
1432
|
+
}
|
|
1433
|
+
if (n === ctx.interp.rt.shellPid || n === ctx.proc.pid) {
|
|
1434
|
+
// the VM's own (virtual) shell: never signal the host process
|
|
1435
|
+
if (signalZero) continue
|
|
1436
|
+
const name = String(sig).replace(/^SIG/, '')
|
|
1437
|
+
const trap = ctx.proc.state.traps.get(name)
|
|
1438
|
+
if (trap !== undefined) {
|
|
1439
|
+
if (trap !== '') await ctx.interp.execList(ctx.interp.rt.parse(trap, `trap ${name}`).body, ctx.proc)
|
|
1440
|
+
continue
|
|
1441
|
+
}
|
|
1442
|
+
throw new ExitSignal(128 + ((os.constants.signals as Record<string, number>)[sig] ?? 15))
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
process.kill(n, sig)
|
|
1446
|
+
} catch (e) {
|
|
1447
|
+
ctx.error(`(${a}) - ${(e as NodeJS.ErrnoException).code === 'ESRCH' ? 'No such process' : String(e)}`)
|
|
1448
|
+
status = 1
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return status
|
|
1452
|
+
})
|
|
1453
|
+
|
|
1454
|
+
def(['trap'], async (ctx) => {
|
|
1455
|
+
const st = ctx.proc.state
|
|
1456
|
+
const args = ctx.argv.slice(1)
|
|
1457
|
+
if (args.length === 0 || args[0] === '-p') {
|
|
1458
|
+
for (const [sig, action] of st.parentTraps ?? st.traps) {
|
|
1459
|
+
if (args[0] === '-p' && args.length > 1 && !args.slice(1).some((a) => a.toUpperCase().replace(/^SIG/, '') === sig)) continue
|
|
1460
|
+
const name = ['EXIT', 'ERR', 'DEBUG', 'RETURN'].includes(sig) ? sig : `SIG${sig}`
|
|
1461
|
+
await ctx.out(`trap -- ${shellQuote(action)} ${name}\n`)
|
|
1462
|
+
}
|
|
1463
|
+
return 0
|
|
1464
|
+
}
|
|
1465
|
+
if (args[0] === '-l') {
|
|
1466
|
+
await ctx.out(Object.keys(os.constants.signals).join(' ') + '\n')
|
|
1467
|
+
return 0
|
|
1468
|
+
}
|
|
1469
|
+
let action: string | null = args[0]
|
|
1470
|
+
let sigs = args.slice(1)
|
|
1471
|
+
if (/^\d+$/.test(action) && sigs.length === 0) {
|
|
1472
|
+
sigs = [action]
|
|
1473
|
+
action = null
|
|
1474
|
+
}
|
|
1475
|
+
if (action === '-') action = null
|
|
1476
|
+
st.parentTraps = undefined
|
|
1477
|
+
for (let s of sigs) {
|
|
1478
|
+
s = s.toUpperCase().replace(/^SIG/, '')
|
|
1479
|
+
if (s === '0') s = 'EXIT'
|
|
1480
|
+
if (action === null) st.traps.delete(s)
|
|
1481
|
+
else st.traps.set(s, action)
|
|
1482
|
+
}
|
|
1483
|
+
return 0
|
|
1484
|
+
})
|
|
1485
|
+
|
|
1486
|
+
def(['getopts'], async (ctx) => {
|
|
1487
|
+
const st = ctx.proc.state
|
|
1488
|
+
const [optstring, name, ...rest] = ctx.argv.slice(1)
|
|
1489
|
+
if (optstring === undefined || name === undefined) {
|
|
1490
|
+
ctx.error('usage: getopts optstring name [arg ...]')
|
|
1491
|
+
return 2
|
|
1492
|
+
}
|
|
1493
|
+
const args = rest.length > 0 ? rest : st.positional
|
|
1494
|
+
let optind = Number(st.getString('OPTIND') ?? '1') || 1
|
|
1495
|
+
const silent = optstring.startsWith(':')
|
|
1496
|
+
const spec = silent ? optstring.slice(1) : optstring
|
|
1497
|
+
const sub = Number(st.getString('__MASH_OPTPOS') ?? '1') || 1
|
|
1498
|
+
const arg = args[optind - 1]
|
|
1499
|
+
const set = (n: string, v: string): void => ctx.interp.assignScalar(ctx.proc, n, v)
|
|
1500
|
+
if (arg === undefined || !arg.startsWith('-') || arg === '-') {
|
|
1501
|
+
set(name, '?')
|
|
1502
|
+
return 1
|
|
1503
|
+
}
|
|
1504
|
+
if (arg === '--') {
|
|
1505
|
+
set('OPTIND', String(optind + 1))
|
|
1506
|
+
set(name, '?')
|
|
1507
|
+
return 1
|
|
1508
|
+
}
|
|
1509
|
+
const ch = arg[sub]
|
|
1510
|
+
let nextSub = sub + 1
|
|
1511
|
+
const advance = (): void => {
|
|
1512
|
+
if (nextSub >= arg.length) {
|
|
1513
|
+
optind++
|
|
1514
|
+
nextSub = 1
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
const idx = spec.indexOf(ch)
|
|
1518
|
+
if (idx === -1 || ch === ':') {
|
|
1519
|
+
if (silent) {
|
|
1520
|
+
set('OPTARG', ch)
|
|
1521
|
+
} else {
|
|
1522
|
+
ctx.error(`illegal option -- ${ch}`)
|
|
1523
|
+
st.unset('OPTARG')
|
|
1524
|
+
}
|
|
1525
|
+
set(name, '?')
|
|
1526
|
+
advance()
|
|
1527
|
+
set('OPTIND', String(optind))
|
|
1528
|
+
set('__MASH_OPTPOS', String(nextSub))
|
|
1529
|
+
return 0
|
|
1530
|
+
}
|
|
1531
|
+
if (spec[idx + 1] === ':') {
|
|
1532
|
+
let optarg: string | undefined
|
|
1533
|
+
if (nextSub < arg.length) {
|
|
1534
|
+
optarg = arg.slice(nextSub)
|
|
1535
|
+
optind++
|
|
1536
|
+
} else {
|
|
1537
|
+
optarg = args[optind]
|
|
1538
|
+
optind += 2
|
|
1539
|
+
}
|
|
1540
|
+
nextSub = 1
|
|
1541
|
+
if (optarg === undefined) {
|
|
1542
|
+
if (silent) {
|
|
1543
|
+
set(name, ':')
|
|
1544
|
+
set('OPTARG', ch)
|
|
1545
|
+
} else {
|
|
1546
|
+
ctx.error(`option requires an argument -- ${ch}`)
|
|
1547
|
+
set(name, '?')
|
|
1548
|
+
}
|
|
1549
|
+
set('OPTIND', String(optind))
|
|
1550
|
+
set('__MASH_OPTPOS', '1')
|
|
1551
|
+
return 0
|
|
1552
|
+
}
|
|
1553
|
+
set('OPTARG', optarg)
|
|
1554
|
+
} else {
|
|
1555
|
+
st.unset('OPTARG')
|
|
1556
|
+
advance()
|
|
1557
|
+
}
|
|
1558
|
+
set(name, ch)
|
|
1559
|
+
set('OPTIND', String(optind))
|
|
1560
|
+
set('__MASH_OPTPOS', String(nextSub))
|
|
1561
|
+
return 0
|
|
1562
|
+
})
|
|
1563
|
+
|
|
1564
|
+
def(['umask'], async (ctx) => {
|
|
1565
|
+
const st = ctx.proc.state
|
|
1566
|
+
const args = ctx.argv.slice(1)
|
|
1567
|
+
const symbolic = args[0] === '-S'
|
|
1568
|
+
if (symbolic) args.shift()
|
|
1569
|
+
if (args[0] === '-p') args.shift()
|
|
1570
|
+
if (args.length === 0) {
|
|
1571
|
+
if (symbolic) {
|
|
1572
|
+
const perm = (shift: number): string => ['r', 'w', 'x'].filter((_, i) => !(st.umask & (4 >> i) << shift)).join('')
|
|
1573
|
+
await ctx.out(`u=${perm(6)},g=${perm(3)},o=${perm(0)}\n`)
|
|
1574
|
+
} else await ctx.out(st.umask.toString(8).padStart(4, '0') + '\n')
|
|
1575
|
+
return 0
|
|
1576
|
+
}
|
|
1577
|
+
if (!/^[0-7]{1,4}$/.test(args[0])) {
|
|
1578
|
+
ctx.error(`${args[0]}: octal number out of range (symbolic modes are not supported)`)
|
|
1579
|
+
return 1
|
|
1580
|
+
}
|
|
1581
|
+
st.umask = parseInt(args[0], 8)
|
|
1582
|
+
return 0
|
|
1583
|
+
})
|
|
1584
|
+
|
|
1585
|
+
def(['times'], async (ctx) => {
|
|
1586
|
+
await ctx.out('0m0.000s 0m0.000s\n0m0.000s 0m0.000s\n')
|
|
1587
|
+
return 0
|
|
1588
|
+
})
|
|
1589
|
+
|
|
1590
|
+
def(['caller'], async () => 1)
|