@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
package/src/vm/state.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell state: variables (with bash's dynamic scoping), functions, options,
|
|
3
|
+
* positional parameters and the working directory. A subshell gets a deep
|
|
4
|
+
* copy via {@link ShellState.clone}.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as os from 'node:os'
|
|
8
|
+
import type * as A from '../parser/ast.ts'
|
|
9
|
+
|
|
10
|
+
/** The host process's umask (read once; children inherit it unless the script changes it). */
|
|
11
|
+
export const HOST_UMASK: number = (() => {
|
|
12
|
+
const m = process.umask(0o022)
|
|
13
|
+
process.umask(m)
|
|
14
|
+
return m
|
|
15
|
+
})()
|
|
16
|
+
|
|
17
|
+
export type VarValue =
|
|
18
|
+
| { kind: 'string'; value: string }
|
|
19
|
+
| { kind: 'indexed'; values: Map<number, string> }
|
|
20
|
+
| { kind: 'assoc'; values: Map<string, string> }
|
|
21
|
+
|
|
22
|
+
export interface Variable {
|
|
23
|
+
/** null: declared but unset (`local x`, `declare -a x` before assignment keeps kind via attrs). */
|
|
24
|
+
value: VarValue | null
|
|
25
|
+
exported: boolean
|
|
26
|
+
readonly: boolean
|
|
27
|
+
integer: boolean
|
|
28
|
+
lower: boolean
|
|
29
|
+
upper: boolean
|
|
30
|
+
/** `declare -a` / `-A` without a value yet. */
|
|
31
|
+
declaredKind?: 'indexed' | 'assoc'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FunctionEntry {
|
|
35
|
+
def: A.FunctionDef
|
|
36
|
+
/** `export -f`: passed to child bash processes (BASH_FUNC_name%%) and inline scripts. */
|
|
37
|
+
exported?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ShellOptions {
|
|
41
|
+
errexit: boolean
|
|
42
|
+
nounset: boolean
|
|
43
|
+
pipefail: boolean
|
|
44
|
+
xtrace: boolean
|
|
45
|
+
noglob: boolean
|
|
46
|
+
noclobber: boolean
|
|
47
|
+
allexport: boolean
|
|
48
|
+
noexec: boolean
|
|
49
|
+
verbose: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const SET_O_NAMES: Record<string, keyof ShellOptions> = {
|
|
53
|
+
errexit: 'errexit', nounset: 'nounset', pipefail: 'pipefail', xtrace: 'xtrace', noglob: 'noglob',
|
|
54
|
+
noclobber: 'noclobber', allexport: 'allexport', noexec: 'noexec', verbose: 'verbose',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const SET_FLAGS: Record<string, keyof ShellOptions> = {
|
|
58
|
+
e: 'errexit', u: 'nounset', x: 'xtrace', f: 'noglob', C: 'noclobber', a: 'allexport', n: 'noexec', v: 'verbose',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const KNOWN_SHOPTS = [
|
|
62
|
+
'nullglob', 'failglob', 'dotglob', 'globstar', 'nocaseglob', 'nocasematch', 'extglob', 'lastpipe', 'patsub_replacement',
|
|
63
|
+
'expand_aliases', 'inherit_errexit', 'globasciiranges', 'checkwinsize', 'histappend', 'sourcepath', 'xpg_echo',
|
|
64
|
+
] as const
|
|
65
|
+
|
|
66
|
+
function cloneValue(v: VarValue | null): VarValue | null {
|
|
67
|
+
if (v === null) return null
|
|
68
|
+
if (v.kind === 'string') return { kind: 'string', value: v.value }
|
|
69
|
+
if (v.kind === 'indexed') return { kind: 'indexed', values: new Map(v.values) }
|
|
70
|
+
return { kind: 'assoc', values: new Map(v.values) }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cloneVar(v: Variable): Variable {
|
|
74
|
+
return { ...v, value: cloneValue(v.value) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** bash's OSTYPE / HOSTTYPE / MACHTYPE for the host (`darwin24` / `linux-gnu`, `x86_64`, ...). */
|
|
78
|
+
function hostTypeVars(): Record<string, string> {
|
|
79
|
+
const darwin = process.platform === 'darwin'
|
|
80
|
+
const hosttype = process.arch === 'x64' ? 'x86_64' : process.arch === 'arm64' ? (darwin ? 'arm64' : 'aarch64') : process.arch
|
|
81
|
+
const ostype = darwin ? `darwin${os.release().split('.')[0]}` : process.platform === 'linux' ? 'linux-gnu' : process.platform
|
|
82
|
+
const vendor = darwin ? 'apple' : hosttype === 'x86_64' ? 'pc' : 'unknown'
|
|
83
|
+
return { OSTYPE: ostype, HOSTTYPE: hosttype, MACHTYPE: `${hosttype}-${vendor}-${ostype}` }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The variables a new bash process sets for itself (after importing the
|
|
88
|
+
* environment): the top-level shell and nested scripts run in the VM.
|
|
89
|
+
*/
|
|
90
|
+
export function initShellVars(state: ShellState): void {
|
|
91
|
+
const set = (k: string, value: VarValue): void => void state.globals.set(k, newVar(value))
|
|
92
|
+
set('BASH_VERSION', { kind: 'string', value: '5.2.0(1)-mash' })
|
|
93
|
+
// like bash's set_if_not: values from the environment win
|
|
94
|
+
for (const [k, v] of Object.entries(hostTypeVars())) if (!state.globals.has(k)) set(k, { kind: 'string', value: v })
|
|
95
|
+
set('PS4', { kind: 'string', value: '+ ' })
|
|
96
|
+
set('IFS', { kind: 'string', value: ' \t\n' })
|
|
97
|
+
set('OPTIND', { kind: 'string', value: '1' })
|
|
98
|
+
// PIPESTATUS is empty until the first command runs (bash 3.2 and 5.2 alike;
|
|
99
|
+
// the "(0)" seen in an earlier CI run came from commands in a sourced ~/.bashrc)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function newVar(value: VarValue | null, attrs: Partial<Variable> = {}): Variable {
|
|
103
|
+
return { value, exported: false, readonly: false, integer: false, lower: false, upper: false, ...attrs }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class ShellState {
|
|
107
|
+
globals = new Map<string, Variable>()
|
|
108
|
+
/** Function-local scopes, innermost last. */
|
|
109
|
+
locals: Map<string, Variable>[] = []
|
|
110
|
+
functions = new Map<string, FunctionEntry>()
|
|
111
|
+
options: ShellOptions = {
|
|
112
|
+
errexit: false, nounset: false, pipefail: false, xtrace: false, noglob: false,
|
|
113
|
+
noclobber: false, allexport: false, noexec: false, verbose: false,
|
|
114
|
+
}
|
|
115
|
+
// patsub_replacement: bash 5.2's default (`&` in ${x/p/r} is the match)
|
|
116
|
+
shopts = new Set<string>(['globasciiranges', 'sourcepath', 'patsub_replacement'])
|
|
117
|
+
positional: string[] = []
|
|
118
|
+
arg0 = 'mash'
|
|
119
|
+
cwd: string
|
|
120
|
+
lastStatus = 0
|
|
121
|
+
lastBgPid: number | null = null
|
|
122
|
+
traps = new Map<string, string>()
|
|
123
|
+
/**
|
|
124
|
+
* In a subshell, the parent's traps, which `trap`/`trap -p` still list until
|
|
125
|
+
* the subshell changes a trap (bash 4.2+; lets `$(trap -p)` save them).
|
|
126
|
+
*/
|
|
127
|
+
parentTraps: Map<string, string> | undefined = undefined
|
|
128
|
+
dirStack: string[] = []
|
|
129
|
+
/** Command hash table (name → resolved path). */
|
|
130
|
+
hash = new Map<string, string>()
|
|
131
|
+
aliases = new Map<string, string>()
|
|
132
|
+
/** Function call stack (for FUNCNAME). */
|
|
133
|
+
funcStack: string[] = []
|
|
134
|
+
/** File creation mask applied to redirections and child processes. */
|
|
135
|
+
umask = HOST_UMASK
|
|
136
|
+
/** Alias expansion depth guard. */
|
|
137
|
+
aliasDepth = 0
|
|
138
|
+
|
|
139
|
+
constructor(cwd: string) {
|
|
140
|
+
this.cwd = cwd
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
clone(): ShellState {
|
|
144
|
+
const s = new ShellState(this.cwd)
|
|
145
|
+
for (const [k, v] of this.globals) s.globals.set(k, cloneVar(v))
|
|
146
|
+
s.locals = this.locals.map((scope) => {
|
|
147
|
+
const m = new Map<string, Variable>()
|
|
148
|
+
for (const [k, v] of scope) m.set(k, cloneVar(v))
|
|
149
|
+
return m
|
|
150
|
+
})
|
|
151
|
+
s.functions = new Map(this.functions)
|
|
152
|
+
s.options = { ...this.options }
|
|
153
|
+
s.shopts = new Set(this.shopts)
|
|
154
|
+
s.positional = [...this.positional]
|
|
155
|
+
s.arg0 = this.arg0
|
|
156
|
+
s.lastStatus = this.lastStatus
|
|
157
|
+
s.lastBgPid = this.lastBgPid
|
|
158
|
+
s.traps = new Map(this.traps)
|
|
159
|
+
s.parentTraps = this.parentTraps
|
|
160
|
+
s.dirStack = [...this.dirStack]
|
|
161
|
+
s.hash = new Map(this.hash)
|
|
162
|
+
s.aliases = new Map(this.aliases)
|
|
163
|
+
s.funcStack = [...this.funcStack]
|
|
164
|
+
s.umask = this.umask
|
|
165
|
+
return s
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Find a variable following dynamic scope (innermost function scope first). */
|
|
169
|
+
lookup(name: string): Variable | undefined {
|
|
170
|
+
for (let i = this.locals.length - 1; i >= 0; i--) {
|
|
171
|
+
const v = this.locals[i].get(name)
|
|
172
|
+
if (v) return v
|
|
173
|
+
}
|
|
174
|
+
return this.globals.get(name)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The scope map holding `name`, or the global scope if none does. */
|
|
178
|
+
scopeOf(name: string): Map<string, Variable> {
|
|
179
|
+
for (let i = this.locals.length - 1; i >= 0; i--) {
|
|
180
|
+
if (this.locals[i].has(name)) return this.locals[i]
|
|
181
|
+
}
|
|
182
|
+
return this.globals
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** String value of a scalar (or element 0 of an array), undefined if unset. */
|
|
186
|
+
getString(name: string): string | undefined {
|
|
187
|
+
const v = this.lookup(name)
|
|
188
|
+
if (!v || v.value === null) return undefined
|
|
189
|
+
if (v.value.kind === 'string') return v.value.value
|
|
190
|
+
if (v.value.kind === 'indexed') return v.value.values.get(0)
|
|
191
|
+
return v.value.values.get('0')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
isSet(name: string): boolean {
|
|
195
|
+
const v = this.lookup(name)
|
|
196
|
+
return v !== undefined && v.value !== null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Declare a local in the innermost function scope (or global when not in a function). */
|
|
200
|
+
declareLocal(name: string): Variable {
|
|
201
|
+
const scope = this.locals.length > 0 ? this.locals[this.locals.length - 1] : this.globals
|
|
202
|
+
let v = scope.get(name)
|
|
203
|
+
if (!v) {
|
|
204
|
+
v = newVar(null)
|
|
205
|
+
scope.set(name, v)
|
|
206
|
+
}
|
|
207
|
+
return v
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Get or create (in the scope that holds it, else global) the variable record. */
|
|
211
|
+
ensure(name: string): Variable {
|
|
212
|
+
const scope = this.scopeOf(name)
|
|
213
|
+
let v = scope.get(name)
|
|
214
|
+
if (!v) {
|
|
215
|
+
v = newVar(null)
|
|
216
|
+
if (this.options.allexport) v.exported = true
|
|
217
|
+
scope.set(name, v)
|
|
218
|
+
}
|
|
219
|
+
return v
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
unset(name: string): boolean {
|
|
223
|
+
const scope = this.scopeOf(name)
|
|
224
|
+
const v = scope.get(name)
|
|
225
|
+
if (v?.readonly) return false
|
|
226
|
+
if (scope === this.globals) scope.delete(name)
|
|
227
|
+
else if (v) {
|
|
228
|
+
// bash: unsetting a local makes it unset in that scope (still shadows)
|
|
229
|
+
v.value = null
|
|
230
|
+
}
|
|
231
|
+
return true
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The environment passed to child processes: exported scalars. */
|
|
235
|
+
environment(): Record<string, string> {
|
|
236
|
+
const env: Record<string, string> = {}
|
|
237
|
+
const visit = (scope: Map<string, Variable>): void => {
|
|
238
|
+
for (const [k, v] of scope) {
|
|
239
|
+
if (!v.exported) {
|
|
240
|
+
// a non-exported local shadows an exported global of the same name
|
|
241
|
+
if (k in env && scope !== this.globals) delete env[k]
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
if (v.value === null) continue
|
|
245
|
+
if (v.value.kind === 'string') env[k] = v.value.value
|
|
246
|
+
else if (v.value.kind === 'indexed') env[k] = v.value.values.get(0) ?? ''
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
visit(this.globals)
|
|
250
|
+
for (const scope of this.locals) visit(scope)
|
|
251
|
+
return env
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** All variable names visible (for `${!prefix*}`, `set`, `declare -p`). */
|
|
255
|
+
visibleNames(): string[] {
|
|
256
|
+
const names = new Set<string>(this.globals.keys())
|
|
257
|
+
for (const scope of this.locals) for (const k of scope.keys()) names.add(k)
|
|
258
|
+
return [...names].sort()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Format `$-` from the options. */
|
|
263
|
+
export function dashFlags(state: ShellState): string {
|
|
264
|
+
let s = 'h'
|
|
265
|
+
for (const [flag, opt] of Object.entries(SET_FLAGS)) if (state.options[opt]) s += flag
|
|
266
|
+
return s + 'B'
|
|
267
|
+
}
|
package/src/vm/test.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `[[ ... ]]` and `test` / `[` evaluation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'node:fs'
|
|
6
|
+
import * as path from 'node:path'
|
|
7
|
+
import type * as A from '../parser/ast.ts'
|
|
8
|
+
import { ArithError } from './arith.ts'
|
|
9
|
+
import type { Interpreter } from './interp.ts'
|
|
10
|
+
import { matchPattern } from './pattern.ts'
|
|
11
|
+
import { CommandError, type Proc } from './proc.ts'
|
|
12
|
+
import { newVar, SET_O_NAMES } from './state.ts'
|
|
13
|
+
|
|
14
|
+
function statSafe(p: string, link = false): fs.Stats | null {
|
|
15
|
+
try {
|
|
16
|
+
return link ? fs.lstatSync(p) : fs.statSync(p)
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function access(p: string, mode: number): boolean {
|
|
23
|
+
try {
|
|
24
|
+
fs.accessSync(p, mode)
|
|
25
|
+
return true
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function fileTest(op: string, operand: string, proc: Proc): boolean {
|
|
32
|
+
const p = path.resolve(proc.state.cwd, operand)
|
|
33
|
+
switch (op) {
|
|
34
|
+
case '-e': case '-a': return statSafe(p) !== null
|
|
35
|
+
case '-f': return statSafe(p)?.isFile() ?? false
|
|
36
|
+
case '-d': return statSafe(p)?.isDirectory() ?? false
|
|
37
|
+
case '-h': case '-L': return statSafe(p, true)?.isSymbolicLink() ?? false
|
|
38
|
+
case '-s': return (statSafe(p)?.size ?? 0) > 0
|
|
39
|
+
case '-r': return access(p, fs.constants.R_OK)
|
|
40
|
+
case '-w': return access(p, fs.constants.W_OK)
|
|
41
|
+
case '-x': return access(p, fs.constants.X_OK)
|
|
42
|
+
case '-p': return statSafe(p)?.isFIFO() ?? false
|
|
43
|
+
case '-S': return statSafe(p)?.isSocket() ?? false
|
|
44
|
+
case '-b': return statSafe(p)?.isBlockDevice() ?? false
|
|
45
|
+
case '-c': return statSafe(p)?.isCharacterDevice() ?? false
|
|
46
|
+
case '-g': return ((statSafe(p)?.mode ?? 0) & 0o2000) !== 0
|
|
47
|
+
case '-u': return ((statSafe(p)?.mode ?? 0) & 0o4000) !== 0
|
|
48
|
+
case '-k': return ((statSafe(p)?.mode ?? 0) & 0o1000) !== 0
|
|
49
|
+
case '-O': return statSafe(p)?.uid === process.getuid?.()
|
|
50
|
+
case '-G': return statSafe(p)?.gid === process.getgid?.()
|
|
51
|
+
case '-N': {
|
|
52
|
+
const s = statSafe(p)
|
|
53
|
+
return s !== null && s.mtimeMs > s.atimeMs
|
|
54
|
+
}
|
|
55
|
+
case '-t': return false
|
|
56
|
+
default: throw new CommandError(`${op}: unary operator expected`, 2)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function unaryTest(op: string, operand: string, proc: Proc): boolean {
|
|
61
|
+
switch (op) {
|
|
62
|
+
case '-n': return operand !== ''
|
|
63
|
+
case '-z': return operand === ''
|
|
64
|
+
case '-v': {
|
|
65
|
+
const m = /^([A-Za-z_][A-Za-z0-9_]*)(?:\[(.*)\])?$/.exec(operand)
|
|
66
|
+
if (!m) return false
|
|
67
|
+
const v = proc.state.lookup(m[1])
|
|
68
|
+
if (!v || v.value === null) return false
|
|
69
|
+
if (m[2] === undefined) return true
|
|
70
|
+
if (v.value.kind === 'indexed') return v.value.values.has(Number(m[2]))
|
|
71
|
+
if (v.value.kind === 'assoc') return v.value.values.has(m[2])
|
|
72
|
+
return m[2] === '0'
|
|
73
|
+
}
|
|
74
|
+
case '-o': {
|
|
75
|
+
const opt = SET_O_NAMES[operand]
|
|
76
|
+
return opt !== undefined && proc.state.options[opt]
|
|
77
|
+
}
|
|
78
|
+
case '-t': return false
|
|
79
|
+
default: return fileTest(op, operand, proc)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function intOf(s: string): bigint {
|
|
84
|
+
const t = s.trim()
|
|
85
|
+
if (!/^[+-]?\d+$/.test(t)) throw new CommandError(`${s}: integer expression expected`, 2)
|
|
86
|
+
return BigInt(t)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function binaryTest(op: string, a: string, b: string, proc: Proc): boolean {
|
|
90
|
+
switch (op) {
|
|
91
|
+
case '=': case '==': return a === b
|
|
92
|
+
case '!=': return a !== b
|
|
93
|
+
case '<': return a < b
|
|
94
|
+
case '>': return a > b
|
|
95
|
+
case '-eq': return intOf(a) === intOf(b)
|
|
96
|
+
case '-ne': return intOf(a) !== intOf(b)
|
|
97
|
+
case '-lt': return intOf(a) < intOf(b)
|
|
98
|
+
case '-le': return intOf(a) <= intOf(b)
|
|
99
|
+
case '-gt': return intOf(a) > intOf(b)
|
|
100
|
+
case '-ge': return intOf(a) >= intOf(b)
|
|
101
|
+
case '-nt': case '-ot': {
|
|
102
|
+
const sa = statSafe(path.resolve(proc.state.cwd, a))
|
|
103
|
+
const sb = statSafe(path.resolve(proc.state.cwd, b))
|
|
104
|
+
if (op === '-nt') return sa !== null && (sb === null || sa.mtimeMs > sb.mtimeMs)
|
|
105
|
+
return sb !== null && (sa === null || sa.mtimeMs < sb.mtimeMs)
|
|
106
|
+
}
|
|
107
|
+
case '-ef': {
|
|
108
|
+
const sa = statSafe(path.resolve(proc.state.cwd, a))
|
|
109
|
+
const sb = statSafe(path.resolve(proc.state.cwd, b))
|
|
110
|
+
return sa !== null && sb !== null && sa.dev === sb.dev && sa.ino === sb.ino
|
|
111
|
+
}
|
|
112
|
+
default: throw new CommandError(`${op}: binary operator expected`, 2)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** POSIX ERE → JS regex source (character classes, mostly). */
|
|
117
|
+
function ereToJs(re: string): string {
|
|
118
|
+
return re.replace(/\[:(alpha|digit|alnum|upper|lower|space|blank|punct|xdigit|word|cntrl|print|graph):\]/g, (_, c: string) => ({
|
|
119
|
+
alpha: 'a-zA-Z', digit: '0-9', alnum: 'a-zA-Z0-9', upper: 'A-Z', lower: 'a-z', space: '\\s', blank: ' \\t',
|
|
120
|
+
punct: '!-\\/:-@\\[-`{-~', xdigit: '0-9a-fA-F', word: '\\w', cntrl: '\\x00-\\x1f', print: '\\x20-\\x7e', graph: '\\x21-\\x7e',
|
|
121
|
+
} as Record<string, string>)[c])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function expandRegex(word: A.Word, proc: Proc, interp: Interpreter): Promise<string> {
|
|
125
|
+
let out = ''
|
|
126
|
+
for (const part of word.parts) {
|
|
127
|
+
const w: A.Word = { ...word, parts: [part] }
|
|
128
|
+
const s = await interp.expander.expandString(w, proc)
|
|
129
|
+
const quoted = part.type === 'SglQuoted' || part.type === 'DblQuoted' || part.type === 'Escaped'
|
|
130
|
+
out += quoted ? s.replace(/[\\^$.*+?()[\]{}|/]/g, (c) => '\\' + c) : s
|
|
131
|
+
}
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ARITH_OPS = new Set(['-eq', '-ne', '-lt', '-le', '-gt', '-ge'])
|
|
136
|
+
|
|
137
|
+
export async function evalCond(e: A.CondExpr, proc: Proc, interp: Interpreter): Promise<boolean> {
|
|
138
|
+
switch (e.type) {
|
|
139
|
+
case 'CondGroup': return evalCond(e.expr, proc, interp)
|
|
140
|
+
case 'CondNot': return !(await evalCond(e.operand, proc, interp))
|
|
141
|
+
case 'CondAndOr': {
|
|
142
|
+
const l = await evalCond(e.left, proc, interp)
|
|
143
|
+
if (e.op === '&&') return l && (await evalCond(e.right, proc, interp))
|
|
144
|
+
return l || (await evalCond(e.right, proc, interp))
|
|
145
|
+
}
|
|
146
|
+
case 'CondWord': return (await interp.expander.expandString(e.word, proc)) !== ''
|
|
147
|
+
case 'CondUnary': return unaryTest(e.op, await interp.expander.expandString(e.operand, proc), proc)
|
|
148
|
+
case 'CondBinary': {
|
|
149
|
+
const left = await interp.expander.expandString(e.left, proc)
|
|
150
|
+
if (e.op === '==' || e.op === '=' || e.op === '!=') {
|
|
151
|
+
const pat = await interp.expander.expandPattern(e.right, proc)
|
|
152
|
+
const m = matchPattern(left, pat, { nocase: proc.state.shopts.has('nocasematch') })
|
|
153
|
+
return e.op === '!=' ? !m : m
|
|
154
|
+
}
|
|
155
|
+
if (e.op === '=~') {
|
|
156
|
+
const src = await expandRegex(e.right, proc, interp)
|
|
157
|
+
let re: RegExp
|
|
158
|
+
try {
|
|
159
|
+
re = new RegExp(ereToJs(src), proc.state.shopts.has('nocasematch') ? 'i' : '')
|
|
160
|
+
} catch {
|
|
161
|
+
throw new CommandError(`invalid regular expression: ${src}`, 2)
|
|
162
|
+
}
|
|
163
|
+
const m = re.exec(left)
|
|
164
|
+
const values = new Map<number, string>()
|
|
165
|
+
if (m) m.forEach((g, i) => values.set(i, g ?? ''))
|
|
166
|
+
proc.state.globals.set('BASH_REMATCH', newVar({ kind: 'indexed', values }))
|
|
167
|
+
return m !== null
|
|
168
|
+
}
|
|
169
|
+
const right = await interp.expander.expandString(e.right, proc)
|
|
170
|
+
if (ARITH_OPS.has(e.op)) {
|
|
171
|
+
try {
|
|
172
|
+
const a = await interp.arithText(left, proc)
|
|
173
|
+
const b = await interp.arithText(right, proc)
|
|
174
|
+
return binaryTest(e.op, String(a), String(b), proc)
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err instanceof ArithError) throw new CommandError(err.message, 2)
|
|
177
|
+
throw err
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return binaryTest(e.op, left, right, proc)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const UNARY = new Set(['-a', '-b', '-c', '-d', '-e', '-f', '-g', '-h', '-k', '-p', '-r', '-s', '-t', '-u', '-w', '-x', '-G', '-L', '-N', '-O', '-S', '-z', '-n', '-o', '-v'])
|
|
186
|
+
const BINARY = new Set(['=', '==', '!=', '<', '>', '-eq', '-ne', '-lt', '-le', '-gt', '-ge', '-nt', '-ot', '-ef'])
|
|
187
|
+
|
|
188
|
+
/** `test` / `[`: returns 0 (true), 1 (false); throws CommandError(2) on syntax errors. */
|
|
189
|
+
export function evalTest(args: string[], proc: Proc): number {
|
|
190
|
+
const tooMany = (): never => {
|
|
191
|
+
throw new CommandError('too many arguments', 2)
|
|
192
|
+
}
|
|
193
|
+
const parseOr = (a: string[]): boolean => {
|
|
194
|
+
// split on -o at top level (lowest precedence), respecting parens
|
|
195
|
+
let depth = 0
|
|
196
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
197
|
+
if (a[i] === ')') depth++
|
|
198
|
+
else if (a[i] === '(') depth--
|
|
199
|
+
else if (a[i] === '-o' && depth === 0 && i > 0 && i < a.length - 1) return parseOr(a.slice(0, i)) || parseAnd(a.slice(i + 1))
|
|
200
|
+
}
|
|
201
|
+
return parseAnd(a)
|
|
202
|
+
}
|
|
203
|
+
const parseAnd = (a: string[]): boolean => {
|
|
204
|
+
let depth = 0
|
|
205
|
+
for (let i = a.length - 1; i >= 0; i--) {
|
|
206
|
+
if (a[i] === ')') depth++
|
|
207
|
+
else if (a[i] === '(') depth--
|
|
208
|
+
else if (a[i] === '-a' && depth === 0 && i > 0 && i < a.length - 1) return parseAnd(a.slice(0, i)) && prim(a.slice(i + 1), true)
|
|
209
|
+
}
|
|
210
|
+
return prim(a, true)
|
|
211
|
+
}
|
|
212
|
+
const prim = (a: string[], fromLogic = false): boolean => {
|
|
213
|
+
switch (a.length) {
|
|
214
|
+
case 0: return false
|
|
215
|
+
case 1: return a[0] !== ''
|
|
216
|
+
case 2:
|
|
217
|
+
if (a[0] === '!') return !prim(a.slice(1))
|
|
218
|
+
if (UNARY.has(a[0])) return unaryTest(a[0], a[1], proc)
|
|
219
|
+
throw new CommandError(`${a[0]}: unary operator expected`, 2)
|
|
220
|
+
case 3:
|
|
221
|
+
if (BINARY.has(a[1])) return binaryTest(a[1], a[0], a[2], proc)
|
|
222
|
+
if (a[0] === '!') return !prim(a.slice(1))
|
|
223
|
+
if (a[0] === '(' && a[2] === ')') return prim([a[1]])
|
|
224
|
+
if (a[1] === '-a' || a[1] === '-o') return parseOr(a)
|
|
225
|
+
throw new CommandError(`${a[1]}: binary operator expected`, 2)
|
|
226
|
+
case 4:
|
|
227
|
+
if (a[0] === '!') return !prim(a.slice(1))
|
|
228
|
+
if (a[0] === '(' && a[3] === ')') return prim(a.slice(1, 3))
|
|
229
|
+
if (fromLogic || (!a.includes('-a') && !a.includes('-o'))) tooMany()
|
|
230
|
+
return parseOr(a)
|
|
231
|
+
default:
|
|
232
|
+
if (a[0] === '!' && !a.includes('-a') && !a.includes('-o')) return !prim(a.slice(1))
|
|
233
|
+
if (a[0] === '(' && a[a.length - 1] === ')') {
|
|
234
|
+
// only if parens balance as a group
|
|
235
|
+
let depth = 0
|
|
236
|
+
let wraps = true
|
|
237
|
+
for (let i = 0; i < a.length - 1; i++) {
|
|
238
|
+
if (a[i] === '(') depth++
|
|
239
|
+
else if (a[i] === ')') depth--
|
|
240
|
+
if (depth === 0) {
|
|
241
|
+
wraps = false
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (wraps) return prim(a.slice(1, -1))
|
|
246
|
+
}
|
|
247
|
+
if (fromLogic || (!a.includes('-a') && !a.includes('-o'))) tooMany()
|
|
248
|
+
return parseOr(a)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const hasLogic = args.length > 4 && (args.includes('-a') || args.includes('-o'))
|
|
252
|
+
return (hasLogic ? parseOr(args) : prim(args)) ? 0 : 1
|
|
253
|
+
}
|