fauxnix-cli 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 +179 -0
- package/dist/ast.d.ts +72 -0
- package/dist/ast.js +43 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +91 -0
- package/dist/commands/archive.d.ts +2 -0
- package/dist/commands/archive.js +385 -0
- package/dist/commands/files.d.ts +2 -0
- package/dist/commands/files.js +721 -0
- package/dist/commands/install-all.d.ts +2 -0
- package/dist/commands/install-all.js +17 -0
- package/dist/commands/net.d.ts +2 -0
- package/dist/commands/net.js +533 -0
- package/dist/commands/sysinfo.d.ts +2 -0
- package/dist/commands/sysinfo.js +1138 -0
- package/dist/commands/text-filters.d.ts +2 -0
- package/dist/commands/text-filters.js +2281 -0
- package/dist/commands/text-io.d.ts +2 -0
- package/dist/commands/text-io.js +1164 -0
- package/dist/encoding.d.ts +7 -0
- package/dist/encoding.js +29 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +103 -0
- package/dist/executor.d.ts +27 -0
- package/dist/executor.js +299 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +79 -0
- package/dist/parser.d.ts +11 -0
- package/dist/parser.js +400 -0
- package/dist/registry.d.ts +79 -0
- package/dist/registry.js +145 -0
- package/dist/translator.d.ts +55 -0
- package/dist/translator.js +318 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import { wordToString } from '../ast.js';
|
|
2
|
+
import { lookup, parseWords, psStr, registeredNames } from '../registry.js';
|
|
3
|
+
import { exprOfWord, operandExpr, translateSimple } from '../translator.js';
|
|
4
|
+
import { handlers as textIoHandlers } from './text-io.js';
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* Shared TS helpers */
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
|
+
/**
|
|
10
|
+
* Split a Word into `NAME=rest` at the first literal '='. Returns null when
|
|
11
|
+
* the word contains no '=' (bare name) or is dynamic before the '='.
|
|
12
|
+
*/
|
|
13
|
+
function splitAssignWord(w) {
|
|
14
|
+
const nameParts = [];
|
|
15
|
+
for (let i = 0; i < w.length; i++) {
|
|
16
|
+
const p = w[i];
|
|
17
|
+
if (p.kind === 'Text' || p.kind === 'SingleQuoted') {
|
|
18
|
+
const eq = p.text.indexOf('=');
|
|
19
|
+
if (eq >= 0) {
|
|
20
|
+
if (eq > 0)
|
|
21
|
+
nameParts.push({ kind: p.kind, text: p.text.slice(0, eq) });
|
|
22
|
+
const value = [];
|
|
23
|
+
const tail = p.text.slice(eq + 1);
|
|
24
|
+
if (tail !== '')
|
|
25
|
+
value.push({ kind: p.kind, text: tail });
|
|
26
|
+
value.push(...w.slice(i + 1));
|
|
27
|
+
const name = nameParts.map((x) => x.text).join('');
|
|
28
|
+
return { name, value };
|
|
29
|
+
}
|
|
30
|
+
nameParts.push(p);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** Text Words -> PS array expression. */
|
|
39
|
+
function textArgs(words) {
|
|
40
|
+
if (words.length === 0)
|
|
41
|
+
return '@()';
|
|
42
|
+
return '@(' + words.map(exprOfWord).join(', ') + ')';
|
|
43
|
+
}
|
|
44
|
+
/** Drop dash-arguments, return operand words. */
|
|
45
|
+
function stripFlags(args) {
|
|
46
|
+
return args.filter((w) => !wordToString(w).startsWith('-'));
|
|
47
|
+
}
|
|
48
|
+
/** Names of commands registered as fauxnix builtins right now. */
|
|
49
|
+
function builtinNames() {
|
|
50
|
+
return registeredNames().filter((n) => lookup(n) !== undefined);
|
|
51
|
+
}
|
|
52
|
+
/* Linux signal table (x86). */
|
|
53
|
+
const SIGNALS = [
|
|
54
|
+
['HUP', 1], ['INT', 2], ['QUIT', 3], ['ILL', 4], ['TRAP', 5], ['ABRT', 6],
|
|
55
|
+
['BUS', 7], ['FPE', 8], ['KILL', 9], ['USR1', 10], ['SEGV', 11], ['USR2', 12],
|
|
56
|
+
['PIPE', 13], ['ALRM', 14], ['TERM', 15], ['STKFLT', 16], ['CHLD', 17],
|
|
57
|
+
['CONT', 18], ['STOP', 19], ['TSTP', 20], ['TTIN', 21], ['TTOU', 22],
|
|
58
|
+
['URG', 23], ['XCPU', 24], ['XFSZ', 25], ['VTALRM', 26], ['PROF', 27],
|
|
59
|
+
['WINCH', 28], ['IO', 29], ['PWR', 30], ['SYS', 31],
|
|
60
|
+
];
|
|
61
|
+
const sigNameToNum = new Map(SIGNALS.map(([n, s]) => [n, s]));
|
|
62
|
+
const sigNumToName = new Map(SIGNALS.map(([n, s]) => [s, n]));
|
|
63
|
+
const killInvalidSignal = (spec) => '[Console]::Error.WriteLine(' +
|
|
64
|
+
psStr('bash: kill: ' + spec + ': invalid signal specification') +
|
|
65
|
+
'); $script:fx_exit = 1';
|
|
66
|
+
/* ------------------------------------------------------------------ */
|
|
67
|
+
/* Shared PS snippets */
|
|
68
|
+
/* ------------------------------------------------------------------ */
|
|
69
|
+
/** PS helper: search PATH (+PATHEXT) for an executable. Returns '' if absent. */
|
|
70
|
+
const PS_WHICH_FN = [
|
|
71
|
+
'function fx-which($n) {',
|
|
72
|
+
" foreach ($fx_d in ($env:PATH -split ';')) {",
|
|
73
|
+
" if ($fx_d -eq '') { continue }",
|
|
74
|
+
' $fx_c = Join-Path $fx_d $n',
|
|
75
|
+
' if (Test-Path -LiteralPath $fx_c) { return $fx_c }',
|
|
76
|
+
" $fx_exts = @(($env:PATHEXT -split ';') + '.exe') | Select-Object -Unique",
|
|
77
|
+
' foreach ($fx_e in $fx_exts) {',
|
|
78
|
+
' if (Test-Path -LiteralPath ($fx_c + $fx_e)) { return ($fx_c + $fx_e) }',
|
|
79
|
+
' }',
|
|
80
|
+
' }',
|
|
81
|
+
" return ''",
|
|
82
|
+
'}',
|
|
83
|
+
].join('\n');
|
|
84
|
+
/** PS helper: month name (English, culture-safe). */
|
|
85
|
+
const PS_MON_FN = "function fx-mon($m) { return @('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec')[[int]$m - 1] }";
|
|
86
|
+
/* ------------------------------------------------------------------ */
|
|
87
|
+
/* cd / pwd */
|
|
88
|
+
/* ------------------------------------------------------------------ */
|
|
89
|
+
const cd = (args) => {
|
|
90
|
+
if (args.length === 0) {
|
|
91
|
+
return [
|
|
92
|
+
'try { Set-Location -LiteralPath $HOME }',
|
|
93
|
+
"catch { [Console]::Error.WriteLine('bash: cd: ' + $_.Exception.Message); $script:fx_exit = 1 }",
|
|
94
|
+
].join('\n');
|
|
95
|
+
}
|
|
96
|
+
if (args.length > 1) {
|
|
97
|
+
return "[Console]::Error.WriteLine('bash: cd: too many arguments'); $script:fx_exit = 1";
|
|
98
|
+
}
|
|
99
|
+
if (wordToString(args[0]) === '-') {
|
|
100
|
+
return [
|
|
101
|
+
"if (-not $env:FAUXNIX_OLDPWD) { [Console]::Error.WriteLine('bash: cd: OLDPWD not set'); $script:fx_exit = 1 }",
|
|
102
|
+
'else { try { Set-Location -LiteralPath $env:FAUXNIX_OLDPWD } catch { [Console]::Error.WriteLine("bash: cd: " + $env:FAUXNIX_OLDPWD + ": No such file or directory"); $script:fx_exit = 1 } }',
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
105
|
+
return [
|
|
106
|
+
'$fx_d = ' + operandExpr(args[0]),
|
|
107
|
+
'if (-not (Test-Path -LiteralPath $fx_d)) { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 }',
|
|
108
|
+
'elseif (-not (Test-Path -LiteralPath $fx_d -PathType Container)) { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": Not a directory"); $script:fx_exit = 1 }',
|
|
109
|
+
'else { try { Set-Location -LiteralPath $fx_d } catch { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 } }',
|
|
110
|
+
].join('\n');
|
|
111
|
+
};
|
|
112
|
+
const pwd = (args) => {
|
|
113
|
+
void args; // -P accepted; physical == logical for Windows providers
|
|
114
|
+
return "(Get-Location).Path.Replace('\\', '/')";
|
|
115
|
+
};
|
|
116
|
+
/* ------------------------------------------------------------------ */
|
|
117
|
+
/* export / unset / env / printenv */
|
|
118
|
+
/* ------------------------------------------------------------------ */
|
|
119
|
+
const ENV_LIST_PS = "Get-ChildItem Env: | Sort-Object -Property Name | ForEach-Object { $_.Name + '=' + [string]$_.Value }";
|
|
120
|
+
const exportCmd = (args) => {
|
|
121
|
+
if (args.length === 0)
|
|
122
|
+
return ENV_LIST_PS;
|
|
123
|
+
const sets = [];
|
|
124
|
+
for (const w of args) {
|
|
125
|
+
const t = wordToString(w);
|
|
126
|
+
if (t.startsWith('-'))
|
|
127
|
+
continue; // -n / -f accepted and ignored
|
|
128
|
+
const sp = splitAssignWord(w);
|
|
129
|
+
if (sp === null)
|
|
130
|
+
continue; // `export VAR` (bare name) -> no-op success
|
|
131
|
+
if (!NAME_RE.test(sp.name)) {
|
|
132
|
+
return ('[Console]::Error.WriteLine(' +
|
|
133
|
+
psStr("bash: export: `" + t + "': not a valid identifier") +
|
|
134
|
+
'); $script:fx_exit = 1');
|
|
135
|
+
}
|
|
136
|
+
sets.push(sp);
|
|
137
|
+
}
|
|
138
|
+
return sets.map((s) => '$env:' + s.name + ' = ' + exprOfWord(s.value)).join('\n');
|
|
139
|
+
};
|
|
140
|
+
const unset = (args) => {
|
|
141
|
+
const names = stripFlags(args);
|
|
142
|
+
if (names.length === 0)
|
|
143
|
+
return '';
|
|
144
|
+
return [
|
|
145
|
+
'$fx_ns = ' + textArgs(names),
|
|
146
|
+
'foreach ($fx_n in $fx_ns) {',
|
|
147
|
+
" if ($fx_n -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { [Console]::Error.WriteLine(\"bash: unset: '\" + $fx_n + \"': not a valid identifier\"); $script:fx_exit = 1; continue }",
|
|
148
|
+
" Remove-Item -LiteralPath ('Env:\\' + $fx_n) -ErrorAction SilentlyContinue",
|
|
149
|
+
'}',
|
|
150
|
+
].join('\n');
|
|
151
|
+
};
|
|
152
|
+
const env = (args, ctx) => {
|
|
153
|
+
const raw = args.map(wordToString);
|
|
154
|
+
const sets = [];
|
|
155
|
+
const unsets = [];
|
|
156
|
+
let i = 0;
|
|
157
|
+
let cmdIdx = -1;
|
|
158
|
+
while (i < raw.length) {
|
|
159
|
+
const t = raw[i];
|
|
160
|
+
if (t === '--') {
|
|
161
|
+
cmdIdx = i + 1;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (t === '-i' || t === '--ignore-environment') {
|
|
165
|
+
i++; // best-effort: the flag is accepted and ignored
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (t === '-u' || t === '--unset') {
|
|
169
|
+
if (i + 1 < raw.length)
|
|
170
|
+
unsets.push(raw[i + 1]);
|
|
171
|
+
i += 2;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (t.startsWith('-u=') || t.startsWith('--unset=')) {
|
|
175
|
+
unsets.push(t.slice(t.indexOf('=') + 1));
|
|
176
|
+
i++;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (t.startsWith('-')) {
|
|
180
|
+
i++; // unknown flags ignored
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const sp = splitAssignWord(args[i]);
|
|
184
|
+
if (sp !== null && NAME_RE.test(sp.name)) {
|
|
185
|
+
sets.push(sp);
|
|
186
|
+
i++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
cmdIdx = i;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
const lines = [];
|
|
193
|
+
for (const u of unsets) {
|
|
194
|
+
lines.push("Remove-Item -LiteralPath ('Env:\\' + " + psStr(u) + ') -ErrorAction SilentlyContinue');
|
|
195
|
+
}
|
|
196
|
+
for (const s of sets)
|
|
197
|
+
lines.push('$env:' + s.name + ' = ' + exprOfWord(s.value));
|
|
198
|
+
if (cmdIdx >= 0 && cmdIdx < args.length) {
|
|
199
|
+
const cmdWords = args.slice(cmdIdx);
|
|
200
|
+
lines.push(translateSimple({ kind: 'SimpleCommand', assignments: [], name: cmdWords[0], args: cmdWords.slice(1), redirects: [] }, ctx.position, ctx.hasStdin));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
lines.push(ENV_LIST_PS);
|
|
204
|
+
}
|
|
205
|
+
return lines.join('\n');
|
|
206
|
+
};
|
|
207
|
+
const printenv = (args) => {
|
|
208
|
+
const names = stripFlags(args);
|
|
209
|
+
if (names.length === 0)
|
|
210
|
+
return ENV_LIST_PS;
|
|
211
|
+
return [
|
|
212
|
+
'$fx_ns = ' + textArgs(names),
|
|
213
|
+
'foreach ($fx_n in $fx_ns) {',
|
|
214
|
+
' $fx_v = [Environment]::GetEnvironmentVariable($fx_n)',
|
|
215
|
+
' if ($null -eq $fx_v) { $script:fx_exit = 1 } else { $fx_v }',
|
|
216
|
+
'}',
|
|
217
|
+
].join('\n');
|
|
218
|
+
};
|
|
219
|
+
/* ------------------------------------------------------------------ */
|
|
220
|
+
/* ps */
|
|
221
|
+
/* ------------------------------------------------------------------ */
|
|
222
|
+
const ps = (args) => {
|
|
223
|
+
const raw = args.map(wordToString);
|
|
224
|
+
const { flags, operandWords } = parseWords(args);
|
|
225
|
+
const full = operandWords.some((w) => wordToString(w) === 'aux') ||
|
|
226
|
+
(flags.has('e') && flags.has('f')) ||
|
|
227
|
+
raw.includes('-ef');
|
|
228
|
+
if (full) {
|
|
229
|
+
return [
|
|
230
|
+
PS_MON_FN,
|
|
231
|
+
'$fx_now = Get-Date',
|
|
232
|
+
'$fx_os = Get-CimInstance Win32_OperatingSystem',
|
|
233
|
+
'$fx_totPhys = [long]$fx_os.TotalVisibleMemorySize * 1024',
|
|
234
|
+
"'{0,-8} {1,5} {2,4:0.0} {3,4:0.0} {4,9} {5,6} {6,-8} {7,-4} {8,6} {9,9} {10}' -f 'USER','PID','%CPU','%MEM','VSZ','RSS','TTY','STAT','START','TIME','COMMAND'",
|
|
235
|
+
'$fx_ps = @(Get-CimInstance Win32_Process | Sort-Object ProcessId)',
|
|
236
|
+
'foreach ($fx_p in $fx_ps) {',
|
|
237
|
+
// NOTE: Win32_Process.GetOwner() costs ~0.3s PER PROCESS via WMI on
|
|
238
|
+
// typical Windows boxes, which makes the table useless — USER stays '?'
|
|
239
|
+
// (the documented fallback) instead of hanging for minutes.
|
|
240
|
+
" $fx_u = '?'",
|
|
241
|
+
' $fx_cpu = 0.0',
|
|
242
|
+
' $fx_el = $fx_now - $fx_p.CreationDate',
|
|
243
|
+
' if ($fx_el.TotalSeconds -gt 0) { $fx_cpu = 100.0 * (([long]$fx_p.KernelModeTime + [long]$fx_p.UserModeTime) / 10000000.0) / $fx_el.TotalSeconds / [Environment]::ProcessorCount }',
|
|
244
|
+
' $fx_mem = 0.0',
|
|
245
|
+
' if ($fx_totPhys -gt 0) { $fx_mem = 100.0 * [long]$fx_p.WorkingSetSize / $fx_totPhys }',
|
|
246
|
+
' $fx_vs = 0; try { $fx_vs = [math]::Round([long]$fx_p.VirtualSize / 1KB) } catch {}',
|
|
247
|
+
' $fx_rs = [math]::Round([long]$fx_p.WorkingSetSize / 1KB)',
|
|
248
|
+
" $fx_st = $fx_p.CreationDate.ToString('HH:mm')",
|
|
249
|
+
" if ($fx_p.CreationDate.Date -ne $fx_now.Date) { $fx_st = (fx-mon $fx_p.CreationDate.Month) + ' ' + ('{0,2}' -f $fx_p.CreationDate.Day) }",
|
|
250
|
+
" $fx_tm = '{0:d}:{1:d2}:{2:d2}' -f [int]$fx_el.TotalHours, $fx_el.Minutes, $fx_el.Seconds",
|
|
251
|
+
' $fx_cmd = [string]$fx_p.CommandLine',
|
|
252
|
+
" if ($fx_cmd -eq '') { $fx_cmd = [string]$fx_p.Name }",
|
|
253
|
+
" '{0,-8} {1,5} {2,4:0.0} {3,4:0.0} {4,9} {5,6} {6,-8} {7,-4} {8,6} {9,9} {10}' -f $fx_u, $fx_p.ProcessId, $fx_cpu, $fx_mem, $fx_vs, $fx_rs, '?', 'S', $fx_st, $fx_tm, $fx_cmd",
|
|
254
|
+
'}',
|
|
255
|
+
].join('\n');
|
|
256
|
+
}
|
|
257
|
+
return [
|
|
258
|
+
"' PID TTY TIME CMD'",
|
|
259
|
+
'$fx_ps = @(Get-CimInstance Win32_Process | Sort-Object ProcessId)',
|
|
260
|
+
'foreach ($fx_p in $fx_ps) {',
|
|
261
|
+
' $fx_ct = [timespan]::FromSeconds(([long]$fx_p.KernelModeTime + [long]$fx_p.UserModeTime) / 10000000.0)',
|
|
262
|
+
" '{0,5} {1,-13}{2,8} {3}' -f $fx_p.ProcessId, '-', ('{0:d2}:{1:d2}:{2:d2}' -f [int]$fx_ct.TotalHours, $fx_ct.Minutes, $fx_ct.Seconds), (([string]$fx_p.Name) -replace '\\.exe$', '')",
|
|
263
|
+
'}',
|
|
264
|
+
].join('\n');
|
|
265
|
+
};
|
|
266
|
+
/* ------------------------------------------------------------------ */
|
|
267
|
+
/* kill / pkill / pgrep */
|
|
268
|
+
/* ------------------------------------------------------------------ */
|
|
269
|
+
const kill = (args) => {
|
|
270
|
+
const raw = args.map(wordToString);
|
|
271
|
+
let i = 0;
|
|
272
|
+
let listMode = false;
|
|
273
|
+
let sig = 'TERM';
|
|
274
|
+
while (i < raw.length) {
|
|
275
|
+
const t = raw[i];
|
|
276
|
+
if (t === '--') {
|
|
277
|
+
i++;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
if (t === '-l' || t === '--list') {
|
|
281
|
+
listMode = true;
|
|
282
|
+
i++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (t === '-s' || t === '--signal') {
|
|
286
|
+
const v0 = (raw[i + 1] ?? '').replace(/^SIG/i, '').toUpperCase();
|
|
287
|
+
let ok = false;
|
|
288
|
+
if (/^\d+$/.test(v0)) {
|
|
289
|
+
const n = parseInt(v0, 10);
|
|
290
|
+
if (sigNumToName.has(n)) {
|
|
291
|
+
sig = sigNumToName.get(n);
|
|
292
|
+
ok = true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (sigNameToNum.has(v0)) {
|
|
296
|
+
sig = v0;
|
|
297
|
+
ok = true;
|
|
298
|
+
}
|
|
299
|
+
if (!ok)
|
|
300
|
+
return killInvalidSignal(raw[i + 1] ?? '');
|
|
301
|
+
i += 2;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const ms = t.match(/^-(\d+)$/);
|
|
305
|
+
if (ms) {
|
|
306
|
+
const n = parseInt(ms[1], 10);
|
|
307
|
+
if (!sigNumToName.has(n))
|
|
308
|
+
return killInvalidSignal(ms[1]);
|
|
309
|
+
sig = sigNumToName.get(n);
|
|
310
|
+
i++;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const mn = t.match(/^-(?:SIG)?([A-Za-z][A-Za-z0-9]*)$/);
|
|
314
|
+
if (mn) {
|
|
315
|
+
const s = mn[1].toUpperCase();
|
|
316
|
+
if (sigNameToNum.has(s)) {
|
|
317
|
+
sig = s;
|
|
318
|
+
i++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
const rest = args.slice(i);
|
|
325
|
+
if (listMode) {
|
|
326
|
+
if (rest.length === 0) {
|
|
327
|
+
const row1 = SIGNALS.slice(0, 16).map(([n]) => n).join(' ');
|
|
328
|
+
const row2 = SIGNALS.slice(16).map(([n]) => n).join(' ');
|
|
329
|
+
return psStr(row1) + '\n' + psStr(row2);
|
|
330
|
+
}
|
|
331
|
+
const lines = [];
|
|
332
|
+
for (const w of rest) {
|
|
333
|
+
const t = wordToString(w);
|
|
334
|
+
if (/^\d+$/.test(t)) {
|
|
335
|
+
const n = parseInt(t, 10);
|
|
336
|
+
if (!sigNumToName.has(n))
|
|
337
|
+
return killInvalidSignal(t);
|
|
338
|
+
lines.push(psStr(sigNumToName.get(n)));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const s = t.replace(/^SIG/i, '').toUpperCase();
|
|
342
|
+
if (!sigNameToNum.has(s))
|
|
343
|
+
return killInvalidSignal(t);
|
|
344
|
+
lines.push(psStr(String(sigNameToNum.get(s))));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return lines.join('\n');
|
|
348
|
+
}
|
|
349
|
+
if (rest.length === 0) {
|
|
350
|
+
return ('[Console]::Error.WriteLine(' +
|
|
351
|
+
psStr('bash: kill: usage: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]') +
|
|
352
|
+
'); $script:fx_exit = 2');
|
|
353
|
+
}
|
|
354
|
+
// Windows cannot deliver SIGTERM to console processes — taskkill without
|
|
355
|
+
// /F only works for GUI apps with a message pump. Both paths force-
|
|
356
|
+
// terminate; -9/-KILL additionally takes down the process tree.
|
|
357
|
+
const force = sig === 'KILL';
|
|
358
|
+
const taskArgs = force ? "@('/PID', $fx_p, '/T', '/F')" : "@('/PID', $fx_p, '/F')";
|
|
359
|
+
return [
|
|
360
|
+
'$fx_pids = ' + textArgs(rest),
|
|
361
|
+
'foreach ($fx_p in $fx_pids) {',
|
|
362
|
+
" if ($fx_p -notmatch '^[0-9]+$') { [Console]::Error.WriteLine('bash: kill: ' + $fx_p + ': arguments must be process or job IDs'); $script:fx_exit = 1; continue }",
|
|
363
|
+
' $fx_r = Start-Process -FilePath taskkill.exe -ArgumentList ' + taskArgs + ' -WindowStyle Hidden -Wait -PassThru',
|
|
364
|
+
' if ($fx_r.ExitCode -ne 0) { [Console]::Error.WriteLine("bash: kill: (" + $fx_p + ") - No such process"); $script:fx_exit = 1 }',
|
|
365
|
+
'}',
|
|
366
|
+
].join('\n');
|
|
367
|
+
};
|
|
368
|
+
function pkillImpl(args, doKill) {
|
|
369
|
+
const raw = args.map(wordToString);
|
|
370
|
+
let full = false;
|
|
371
|
+
let force = false;
|
|
372
|
+
let pat = null;
|
|
373
|
+
for (let k = 0; k < args.length; k++) {
|
|
374
|
+
const t = raw[k];
|
|
375
|
+
if (t === '--') {
|
|
376
|
+
if (k + 1 < args.length)
|
|
377
|
+
pat = args[k + 1];
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
if (t === '-f' || t === '--full' || t === '--list-full') {
|
|
381
|
+
full = true;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (/^-\d+$/.test(t)) {
|
|
385
|
+
if (parseInt(t.slice(1), 10) === 9)
|
|
386
|
+
force = true;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (/^-(SIG)?KILL$/i.test(t)) {
|
|
390
|
+
force = true;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (/^-(SIG)?[A-Za-z0-9]+$/.test(t))
|
|
394
|
+
continue; // other signals -> graceful
|
|
395
|
+
if (t.startsWith('-'))
|
|
396
|
+
continue;
|
|
397
|
+
pat = args[k];
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
if (pat === null) {
|
|
401
|
+
const cmd = doKill ? 'pkill' : 'pgrep';
|
|
402
|
+
return ('[Console]::Error.WriteLine(' +
|
|
403
|
+
psStr('usage: ' + cmd + ' [-f] [-signal] pattern') +
|
|
404
|
+
'); $script:fx_exit = 2');
|
|
405
|
+
}
|
|
406
|
+
const taskArgs = force
|
|
407
|
+
? "@('/PID', [string]$fx_p.ProcessId, '/T', '/F')"
|
|
408
|
+
: "@('/PID', [string]$fx_p.ProcessId, '/F')";
|
|
409
|
+
return [
|
|
410
|
+
'$fx_pat = ' + exprOfWord(pat),
|
|
411
|
+
'$fx_ps = @(Get-CimInstance Win32_Process | Sort-Object ProcessId)',
|
|
412
|
+
// never kill our own ancestor chain (the agent invoking fauxnix)
|
|
413
|
+
'$fx_mine = @()',
|
|
414
|
+
'$fx_cur = $PID',
|
|
415
|
+
'while ($fx_cur) {',
|
|
416
|
+
' $fx_mine += $fx_cur',
|
|
417
|
+
' $fx_pp = 0',
|
|
418
|
+
' foreach ($fx_x in $fx_ps) { if ($fx_x.ProcessId -eq $fx_cur) { $fx_pp = $fx_x.ParentProcessId; break } }',
|
|
419
|
+
" if ($fx_pp -and $fx_pp -ne 0 -and ($fx_mine -notcontains $fx_pp)) { $fx_cur = $fx_pp } else { $fx_cur = $null }",
|
|
420
|
+
'}',
|
|
421
|
+
'$fx_hits = @()',
|
|
422
|
+
'foreach ($fx_p in $fx_ps) {',
|
|
423
|
+
full ? " $fx_s = [string]$fx_p.CommandLine" : " $fx_s = ([string]$fx_p.Name) -replace '\\.exe$', ''",
|
|
424
|
+
' $fx_m = $false',
|
|
425
|
+
' try { $fx_m = [regex]::IsMatch($fx_s, $fx_pat) } catch {}',
|
|
426
|
+
// agent-safety guards (stronger than GNU pkill): never match the host
|
|
427
|
+
// shell wrappers (`bash -c "<user command>"` carries the pattern text in
|
|
428
|
+
// argv) nor fauxnix's own -EncodedCommand runner processes.
|
|
429
|
+
" $fx_wrap = ((@('bash.exe','sh.exe','cmd.exe') -contains [string]$fx_p.Name) -and (([string]$fx_p.CommandLine) -match ' -c '))",
|
|
430
|
+
" $fx_self = ((@('powershell.exe','pwsh.exe') -contains [string]$fx_p.Name) -and (([string]$fx_p.CommandLine) -match '-EncodedCommand'))",
|
|
431
|
+
' if ($fx_m -and -not $fx_wrap -and -not $fx_self -and ($fx_mine -notcontains $fx_p.ProcessId)) { $fx_hits += ,$fx_p }',
|
|
432
|
+
'}', 'foreach ($fx_p in $fx_hits) {',
|
|
433
|
+
doKill
|
|
434
|
+
? ' $fx_r = Start-Process -FilePath taskkill.exe -ArgumentList ' + taskArgs + ' -WindowStyle Hidden -Wait -PassThru'
|
|
435
|
+
: ' [string]$fx_p.ProcessId',
|
|
436
|
+
doKill
|
|
437
|
+
? ' if ($fx_r.ExitCode -ne 0) { [Console]::Error.WriteLine("pkill: (" + $fx_p.ProcessId + ") - No such process"); $script:fx_exit = 1 }'
|
|
438
|
+
: '',
|
|
439
|
+
'}',
|
|
440
|
+
'if ($fx_hits.Count -eq 0) { $script:fx_exit = 1 }',
|
|
441
|
+
]
|
|
442
|
+
.filter((l) => l !== '')
|
|
443
|
+
.join('\n');
|
|
444
|
+
}
|
|
445
|
+
const pkill = (args) => pkillImpl(args, true);
|
|
446
|
+
const pgrep = (args) => pkillImpl(args, false);
|
|
447
|
+
/* ------------------------------------------------------------------ */
|
|
448
|
+
/* sleep */
|
|
449
|
+
/* ------------------------------------------------------------------ */
|
|
450
|
+
const sleep = (args) => {
|
|
451
|
+
const ws = stripFlags(args);
|
|
452
|
+
if (ws.length === 0) {
|
|
453
|
+
return "[Console]::Error.WriteLine('sleep: missing operand'); $script:fx_exit = 1";
|
|
454
|
+
}
|
|
455
|
+
return [
|
|
456
|
+
'$fx_t = 0.0',
|
|
457
|
+
'$fx_as = ' + textArgs(ws),
|
|
458
|
+
'foreach ($fx_a in $fx_as) {',
|
|
459
|
+
" if ($fx_a -notmatch '^[0-9]*\\.?[0-9]+[smhd]?$') { [Console]::Error.WriteLine(\"sleep: invalid time interval '\" + $fx_a + \"'\"); $script:fx_exit = 1; continue }",
|
|
460
|
+
" $fx_u = 's'",
|
|
461
|
+
" if ($fx_a -match '[smhd]$') { $fx_u = $fx_a.Substring($fx_a.Length - 1); $fx_a = $fx_a.Substring(0, $fx_a.Length - 1) }",
|
|
462
|
+
' $fx_v = [double]$fx_a',
|
|
463
|
+
" if ($fx_u -eq 'm') { $fx_v = $fx_v * 60 } elseif ($fx_u -eq 'h') { $fx_v = $fx_v * 3600 } elseif ($fx_u -eq 'd') { $fx_v = $fx_v * 86400 }",
|
|
464
|
+
' $fx_t += $fx_v',
|
|
465
|
+
'}',
|
|
466
|
+
'if ($script:fx_exit -eq 0) { Start-Sleep -Seconds $fx_t }',
|
|
467
|
+
].join('\n');
|
|
468
|
+
};
|
|
469
|
+
/* ------------------------------------------------------------------ */
|
|
470
|
+
/* which / type */
|
|
471
|
+
/* ------------------------------------------------------------------ */
|
|
472
|
+
const which = (args) => {
|
|
473
|
+
const ws = stripFlags(args);
|
|
474
|
+
if (ws.length === 0)
|
|
475
|
+
return '';
|
|
476
|
+
return [
|
|
477
|
+
PS_WHICH_FN,
|
|
478
|
+
'$fx_b = @(' + builtinNames().map(psStr).join(', ') + ')',
|
|
479
|
+
'$fx_ns = ' + textArgs(ws),
|
|
480
|
+
'foreach ($fx_n in $fx_ns) {',
|
|
481
|
+
" if ($fx_b -contains $fx_n) { '/usr/bin/' + $fx_n }",
|
|
482
|
+
" elseif ((fx-which $fx_n) -eq '') { [Console]::Error.WriteLine('which: no ' + $fx_n + ' in (' + $env:PATH + ')'); $script:fx_exit = 1 }",
|
|
483
|
+
' else { fx-which $fx_n }',
|
|
484
|
+
'}',
|
|
485
|
+
].join('\n');
|
|
486
|
+
};
|
|
487
|
+
const type = (args) => {
|
|
488
|
+
const ws = stripFlags(args);
|
|
489
|
+
if (ws.length === 0)
|
|
490
|
+
return '';
|
|
491
|
+
return [
|
|
492
|
+
PS_WHICH_FN,
|
|
493
|
+
'$fx_b = @(' + builtinNames().map(psStr).join(', ') + ')',
|
|
494
|
+
'$fx_ns = ' + textArgs(ws),
|
|
495
|
+
'foreach ($fx_n in $fx_ns) {',
|
|
496
|
+
" if ($fx_b -contains $fx_n) { $fx_n + ' is /usr/bin/' + $fx_n }",
|
|
497
|
+
' else {',
|
|
498
|
+
' $fx_w = fx-which $fx_n',
|
|
499
|
+
" if ($fx_w -eq '') { [Console]::Error.WriteLine('bash: type: ' + $fx_n + ': not found'); $script:fx_exit = 1 }",
|
|
500
|
+
" else { $fx_n + ' is ' + $fx_w.Replace('\\', '/') }",
|
|
501
|
+
' }',
|
|
502
|
+
'}',
|
|
503
|
+
].join('\n');
|
|
504
|
+
};
|
|
505
|
+
/* ------------------------------------------------------------------ */
|
|
506
|
+
/* whoami / id / groups */
|
|
507
|
+
/* ------------------------------------------------------------------ */
|
|
508
|
+
const whoami = () => '$env:USERNAME.ToLower()';
|
|
509
|
+
const id = (args) => {
|
|
510
|
+
const { flags } = parseWords(args);
|
|
511
|
+
const wantNum = flags.has('u') || flags.has('g') || flags.has('G');
|
|
512
|
+
const wantName = flags.has('n');
|
|
513
|
+
const uidPs = [
|
|
514
|
+
'$fx_sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value',
|
|
515
|
+
"$fx_ch = $fx_sid.Split('-')",
|
|
516
|
+
'$fx_uid = [int]([long]($fx_ch[$fx_ch.Count - 1]) % 60000) + 1000',
|
|
517
|
+
];
|
|
518
|
+
if (wantNum) {
|
|
519
|
+
return [...uidPs, wantName ? '$env:USERNAME' : '[string]$fx_uid'].join('\n');
|
|
520
|
+
}
|
|
521
|
+
return [
|
|
522
|
+
...uidPs,
|
|
523
|
+
"'uid=' + $fx_uid + '(' + $env:USERNAME + ') gid=' + $fx_uid + '(' + $env:USERNAME + ') groups=' + $fx_uid + '(' + $env:USERNAME + ')'",
|
|
524
|
+
].join('\n');
|
|
525
|
+
};
|
|
526
|
+
const groups = () => {
|
|
527
|
+
return [
|
|
528
|
+
'$fx_id = [System.Security.Principal.WindowsIdentity]::GetCurrent()',
|
|
529
|
+
'$fx_gs = @()',
|
|
530
|
+
'foreach ($fx_g in $fx_id.Groups) {',
|
|
531
|
+
' try { $fx_gs += ,$fx_g.Translate([System.Security.Principal.NTAccount]).Value } catch {}',
|
|
532
|
+
'}',
|
|
533
|
+
"if ($fx_gs.Count -eq 0) { $fx_gs = @('None') }",
|
|
534
|
+
'$fx_names = @()',
|
|
535
|
+
'foreach ($fx_g in $fx_gs) {',
|
|
536
|
+
' $fx_x = $fx_g',
|
|
537
|
+
" $fx_parts = $fx_g -split '\\\\'",
|
|
538
|
+
' if ($fx_parts.Count -gt 1) { $fx_x = $fx_parts[$fx_parts.Count - 1] }',
|
|
539
|
+
' $fx_names += $fx_x',
|
|
540
|
+
'}',
|
|
541
|
+
"$env:USERNAME + ' : ' + ($fx_names -join ' ')",
|
|
542
|
+
].join('\n');
|
|
543
|
+
};
|
|
544
|
+
/* ------------------------------------------------------------------ */
|
|
545
|
+
/* date */
|
|
546
|
+
/* ------------------------------------------------------------------ */
|
|
547
|
+
const DATE_FNS = [
|
|
548
|
+
PS_MON_FN,
|
|
549
|
+
"function fx-monf($m) { return @('January','February','March','April','May','June','July','August','September','October','November','December')[[int]$m - 1] }",
|
|
550
|
+
"function fx-dow($d) { return @('Mon','Tue','Wed','Thu','Fri','Sat','Sun')[(([int]$d.DayOfWeek) + 6) % 7] }",
|
|
551
|
+
"function fx-dowf($d) { return @('Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday')[(([int]$d.DayOfWeek) + 6) % 7] }",
|
|
552
|
+
'function fx-tz($d) {',
|
|
553
|
+
' $fx_tzi = [TimeZoneInfo]::Local',
|
|
554
|
+
' $fx_n = $fx_tzi.StandardName',
|
|
555
|
+
' if ($d.IsDaylightSavingTime()) { $fx_n = $fx_tzi.DaylightName }',
|
|
556
|
+
" if ($fx_n -match 'Coordinated') { return 'UTC' }",
|
|
557
|
+
' $fx_ok = $true',
|
|
558
|
+
' foreach ($fx_c in $fx_n.ToCharArray()) { if ([int]$fx_c -gt 127 -or [int]$fx_c -lt 33) { $fx_ok = $false; break } }',
|
|
559
|
+
" if (-not $fx_ok -or $fx_n.Trim() -eq '') {",
|
|
560
|
+
' $fx_o = $fx_tzi.GetUtcOffset($d)',
|
|
561
|
+
" $fx_s = '+'; if ($fx_o.TotalMinutes -lt 0) { $fx_s = '-' }",
|
|
562
|
+
' $fx_m = [math]::Abs([int]$fx_o.TotalMinutes)',
|
|
563
|
+
" $fx_r = $fx_s + ('{0:d2}' -f [int]($fx_m / 60))",
|
|
564
|
+
" if (($fx_m % 60) -ne 0) { $fx_r = $fx_r + ('{0:d2}' -f ($fx_m % 60)) }",
|
|
565
|
+
' return $fx_r',
|
|
566
|
+
' }',
|
|
567
|
+
' $fx_a = \'\'',
|
|
568
|
+
" foreach ($fx_w in ($fx_n -split '\\s+')) { if ($fx_w.Length -gt 0) { $fx_a += $fx_w.Substring(0, 1) } }",
|
|
569
|
+
' return $fx_a.ToUpper()',
|
|
570
|
+
'}',
|
|
571
|
+
'function fx-datefmt($f, $d, $z) {',
|
|
572
|
+
' $fx_o = \'\'',
|
|
573
|
+
' for ($fx_i = 0; $fx_i -lt $f.Length; $fx_i++) {',
|
|
574
|
+
' $fx_c = [string]$f[$fx_i]',
|
|
575
|
+
" if ($fx_c -ne '%') { $fx_o += $fx_c; continue }",
|
|
576
|
+
" if ($fx_i + 1 -ge $f.Length) { $fx_o += '%'; break }",
|
|
577
|
+
' $fx_t = [string]$f[$fx_i + 1]; $fx_i++',
|
|
578
|
+
' switch -CaseSensitive ($fx_t) {',
|
|
579
|
+
" 'Y' { $fx_o += ('{0:d4}' -f $d.Year) }",
|
|
580
|
+
" 'm' { $fx_o += ('{0:d2}' -f $d.Month) }",
|
|
581
|
+
" 'd' { $fx_o += ('{0:d2}' -f $d.Day) }",
|
|
582
|
+
" 'H' { $fx_o += ('{0:d2}' -f $d.Hour) }",
|
|
583
|
+
" 'M' { $fx_o += ('{0:d2}' -f $d.Minute) }",
|
|
584
|
+
" 'S' { $fx_o += ('{0:d2}' -f $d.Second) }",
|
|
585
|
+
" 'y' { $fx_o += ('{0:d2}' -f ($d.Year % 100)) }",
|
|
586
|
+
" 'j' { $fx_o += ('{0:d3}' -f $d.DayOfYear) }",
|
|
587
|
+
" 's' { $fx_o += [string][long](($d.ToUniversalTime() - [datetime]'1970-01-01').TotalSeconds) }",
|
|
588
|
+
" 'F' { $fx_o += (('{0:d4}' -f $d.Year) + '-' + ('{0:d2}' -f $d.Month) + '-' + ('{0:d2}' -f $d.Day)) }",
|
|
589
|
+
" 'T' { $fx_o += (('{0:d2}' -f $d.Hour) + ':' + ('{0:d2}' -f $d.Minute) + ':' + ('{0:d2}' -f $d.Second)) }",
|
|
590
|
+
" 'Z' { $fx_o += $z }",
|
|
591
|
+
" 'a' { $fx_o += (fx-dow $d) }",
|
|
592
|
+
" 'A' { $fx_o += (fx-dowf $d) }",
|
|
593
|
+
" 'b' { $fx_o += (fx-mon $d.Month) }",
|
|
594
|
+
" 'B' { $fx_o += (fx-monf $d.Month) }",
|
|
595
|
+
" 'p' { if ($d.Hour -lt 12) { $fx_o += 'AM' } else { $fx_o += 'PM' } }",
|
|
596
|
+
" 'I' { $fx_o += ('{0:d2}' -f ((($d.Hour + 11) % 12) + 1)) }",
|
|
597
|
+
" 'u' { $fx_o += [string](((([int]$d.DayOfWeek) + 6) % 7) + 1) }",
|
|
598
|
+
" 'w' { $fx_o += [string][int]$d.DayOfWeek }",
|
|
599
|
+
" 'e' { $fx_o += ('{0,2}' -f $d.Day) }",
|
|
600
|
+
" 'D' { $fx_o += (('{0:d2}' -f $d.Month) + '/' + ('{0:d2}' -f $d.Day) + '/' + ('{0:d2}' -f ($d.Year % 100))) }",
|
|
601
|
+
" '%' { $fx_o += '%' }",
|
|
602
|
+
" default { $fx_o += ('%' + $fx_t) }",
|
|
603
|
+
' }',
|
|
604
|
+
' }',
|
|
605
|
+
' return $fx_o',
|
|
606
|
+
'}',
|
|
607
|
+
].join('\n');
|
|
608
|
+
const date = (args) => {
|
|
609
|
+
const { flags, operandWords } = parseWords(args, ['d']);
|
|
610
|
+
const utc = flags.has('u');
|
|
611
|
+
// find the -d value as a Word (so dynamic values stay PS expressions)
|
|
612
|
+
const raw = args.map(wordToString);
|
|
613
|
+
let dWord = null;
|
|
614
|
+
for (let k = 0; k < raw.length; k++) {
|
|
615
|
+
if (raw[k] === '-d' || raw[k] === '--date') {
|
|
616
|
+
dWord = args[k + 1] ?? null;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const fmtWord = operandWords.find((w) => wordToString(w).startsWith('+'));
|
|
621
|
+
const lines = [DATE_FNS];
|
|
622
|
+
lines.push('$fx_d = ' + (utc ? '[DateTime]::UtcNow' : 'Get-Date'));
|
|
623
|
+
lines.push('$fx_ok = $true');
|
|
624
|
+
if (dWord !== null) {
|
|
625
|
+
lines.push('$fx_ds = ' + exprOfWord(dWord));
|
|
626
|
+
lines.push("if ($fx_ds -like '@*') {");
|
|
627
|
+
lines.push(" try { $fx_n = [long]($fx_ds.Substring(1)); $fx_d = ([datetime]'1970-01-01').AddSeconds($fx_n)" +
|
|
628
|
+
(utc ? ' }' : '.ToLocalTime() }'));
|
|
629
|
+
lines.push(' catch { $fx_ok = $false }');
|
|
630
|
+
lines.push('} else { $fx_ok = $false }');
|
|
631
|
+
lines.push(' if (-not $fx_ok) { [Console]::Error.WriteLine("date: invalid date \'" + $fx_ds + "\'"); $script:fx_exit = 1 }');
|
|
632
|
+
}
|
|
633
|
+
lines.push('if ($fx_ok) {');
|
|
634
|
+
lines.push(' $fx_z = ' + (utc ? "'UTC'" : '(fx-tz $fx_d)'));
|
|
635
|
+
if (fmtWord) {
|
|
636
|
+
lines.push(' $fx_f = ' + exprOfWord(fmtWord));
|
|
637
|
+
lines.push(' fx-datefmt ($fx_f.Substring(1)) $fx_d $fx_z');
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
lines.push(" fx-datefmt '%a %b %e %H:%M:%S %Z %Y' $fx_d $fx_z");
|
|
641
|
+
}
|
|
642
|
+
lines.push('}');
|
|
643
|
+
return lines.join('\n');
|
|
644
|
+
};
|
|
645
|
+
/* ------------------------------------------------------------------ */
|
|
646
|
+
/* uname / hostname / uptime / free / nproc */
|
|
647
|
+
/* ------------------------------------------------------------------ */
|
|
648
|
+
const ARCH_PS = "($(if ($env:PROCESSOR_ARCHITECTURE -like 'ARM*') { 'ARM64' } else { 'x86_64' }))";
|
|
649
|
+
const uname = (args) => {
|
|
650
|
+
const { flags } = parseWords(args);
|
|
651
|
+
if (flags.has('a')) {
|
|
652
|
+
return ("('Linux ' + $env:COMPUTERNAME + ' 6.8.0-fauxnix #1 SMP PREEMPT_DYNAMIC ' + " +
|
|
653
|
+
ARCH_PS +
|
|
654
|
+
" + ' GNU/Linux')");
|
|
655
|
+
}
|
|
656
|
+
const parts = [];
|
|
657
|
+
if (flags.has('s') || flags.size === 0)
|
|
658
|
+
parts.push("'Linux'");
|
|
659
|
+
if (flags.has('n'))
|
|
660
|
+
parts.push('$env:COMPUTERNAME');
|
|
661
|
+
if (flags.has('r'))
|
|
662
|
+
parts.push("'6.8.0-fauxnix'");
|
|
663
|
+
if (flags.has('v'))
|
|
664
|
+
parts.push("'#1 SMP PREEMPT_DYNAMIC'");
|
|
665
|
+
if (flags.has('m') || flags.has('p'))
|
|
666
|
+
parts.push(ARCH_PS);
|
|
667
|
+
if (flags.has('o'))
|
|
668
|
+
parts.push("'GNU/Linux'");
|
|
669
|
+
if (parts.length === 0)
|
|
670
|
+
parts.push("'Linux'");
|
|
671
|
+
return '(@(' + parts.join(', ') + ") -join ' ')";
|
|
672
|
+
};
|
|
673
|
+
const hostname = () => '$env:COMPUTERNAME';
|
|
674
|
+
const uptime = () => {
|
|
675
|
+
return [
|
|
676
|
+
'$fx_now = Get-Date',
|
|
677
|
+
'$fx_os = Get-CimInstance Win32_OperatingSystem',
|
|
678
|
+
'$fx_el = $fx_now - $fx_os.LastBootUpTime',
|
|
679
|
+
"$fx_up = '{0} min' -f [int][math]::Floor($fx_el.TotalMinutes)",
|
|
680
|
+
"if ($fx_el.TotalHours -ge 1) { $fx_up = '{0:d}:{1:d2}' -f $fx_el.Hours, $fx_el.Minutes }",
|
|
681
|
+
"if ($fx_el.Days -ge 2) { $fx_up = ('{0} days, ' -f $fx_el.Days) + ('{0:d}:{1:d2}' -f $fx_el.Hours, $fx_el.Minutes) }",
|
|
682
|
+
"elseif ($fx_el.Days -eq 1) { $fx_up = '1 day, ' + ('{0:d}:{1:d2}' -f $fx_el.Hours, $fx_el.Minutes) }",
|
|
683
|
+
"'{0} up {1}, 0 users, load average: 0.00, 0.00, 0.00' -f $fx_now.ToString('HH:mm:ss'), $fx_up",
|
|
684
|
+
].join('\n');
|
|
685
|
+
};
|
|
686
|
+
const free = (args) => {
|
|
687
|
+
const { flags } = parseWords(args);
|
|
688
|
+
const unit = flags.has('h') ? 'h' : flags.has('g') ? 'g' : flags.has('m') ? 'm' : 'k';
|
|
689
|
+
// value converter for KB-based memory values
|
|
690
|
+
let conv;
|
|
691
|
+
if (unit === 'k')
|
|
692
|
+
conv = (v) => '[string]' + v;
|
|
693
|
+
else if (unit === 'm')
|
|
694
|
+
conv = (v) => '[string][math]::Round(' + v + ' / 1KB)';
|
|
695
|
+
else if (unit === 'g')
|
|
696
|
+
conv = (v) => '[string][math]::Round(' + v + ' / 1MB)';
|
|
697
|
+
else
|
|
698
|
+
conv = (v) => '(fx-fh ' + v + ')';
|
|
699
|
+
// value converter for MB-based swap values
|
|
700
|
+
let convS;
|
|
701
|
+
if (unit === 'k')
|
|
702
|
+
convS = (v) => '[string](' + v + ' * 1KB)';
|
|
703
|
+
else if (unit === 'm')
|
|
704
|
+
convS = (v) => '[string]' + v;
|
|
705
|
+
else if (unit === 'g')
|
|
706
|
+
convS = (v) => '[string][math]::Round(' + v + ' / 1KB)';
|
|
707
|
+
else
|
|
708
|
+
convS = (v) => '(fx-fh (' + v + ' * 1KB))';
|
|
709
|
+
const lines = [];
|
|
710
|
+
if (unit === 'h') {
|
|
711
|
+
lines.push('function fx-fh($kb) {', " if ($kb -ge 1MB) { return ('{0:0.#}Gi' -f ($kb / 1MB)) }", " if ($kb -ge 1KB) { return ('{0:0.#}Mi' -f ($kb / 1KB)) }", " if ($kb -le 0) { return '0B' }", " return ('{0:0.#}Ki' -f $kb)", '}');
|
|
712
|
+
}
|
|
713
|
+
lines.push('$fx_os = Get-CimInstance Win32_OperatingSystem', '$fx_tk = [long]$fx_os.TotalVisibleMemorySize', '$fx_fk = [long]$fx_os.FreePhysicalMemory', '$fx_uk = $fx_tk - $fx_fk', '$fx_st = 0', '$fx_su = 0', 'foreach ($fx_p in @(Get-CimInstance Win32_PageFileUsage)) { $fx_st += [long]$fx_p.AllocatedBaseSize; $fx_su += [long]$fx_p.CurrentUsage }', "'{0,-8}{1,13}{2,13}{3,13}{4,13}{5,13}{6,13}' -f '', 'total', 'used', 'free', 'shared', 'buff/cache', 'available'", "'{0,-8}{1,13}{2,13}{3,13}{4,13}{5,13}{6,13}' -f 'Mem:', " +
|
|
714
|
+
conv('$fx_tk') +
|
|
715
|
+
', ' +
|
|
716
|
+
conv('$fx_uk') +
|
|
717
|
+
', ' +
|
|
718
|
+
conv('$fx_fk') +
|
|
719
|
+
", '0', '0', " +
|
|
720
|
+
conv('$fx_fk'), "('{0,-8}{1,13}{2,13}{3,13}' -f 'Swap:', " +
|
|
721
|
+
convS('$fx_st') +
|
|
722
|
+
', ' +
|
|
723
|
+
convS('$fx_su') +
|
|
724
|
+
', ' +
|
|
725
|
+
convS('($fx_st - $fx_su)') +
|
|
726
|
+
").TrimEnd()");
|
|
727
|
+
return lines.join('\n');
|
|
728
|
+
};
|
|
729
|
+
const nproc = () => '[string][Environment]::ProcessorCount';
|
|
730
|
+
/* ------------------------------------------------------------------ */
|
|
731
|
+
/* clear / true / false / : */
|
|
732
|
+
/* ------------------------------------------------------------------ */
|
|
733
|
+
const clear = () => "([char]27 + '[2J' + [char]27 + '[H')";
|
|
734
|
+
const trueCmd = () => '';
|
|
735
|
+
const falseCmd = () => '$script:fx_exit = 1';
|
|
736
|
+
const colon = () => '';
|
|
737
|
+
/* ------------------------------------------------------------------ */
|
|
738
|
+
/* test / [ */
|
|
739
|
+
/* ------------------------------------------------------------------ */
|
|
740
|
+
const TEST_UNARY = new Set(['-e', '-f', '-d', '-r', '-w', '-x', '-s', '-z', '-n']);
|
|
741
|
+
const TEST_BINARY = new Set(['=', '==', '!=', '-eq', '-ne', '-lt', '-le', '-gt', '-ge']);
|
|
742
|
+
const FX_TN_FN = [
|
|
743
|
+
'function fx-tn($a, $b, $op) {',
|
|
744
|
+
" if ($a -notmatch '^[+-]?[0-9]+$') { [Console]::Error.WriteLine('bash: [: ' + [string]$a + ': integer expression expected'); $script:fx_exit = 2; return $false }",
|
|
745
|
+
" if ($b -notmatch '^[+-]?[0-9]+$') { [Console]::Error.WriteLine('bash: [: ' + [string]$b + ': integer expression expected'); $script:fx_exit = 2; return $false }",
|
|
746
|
+
' $fx_x = [long]$a; $fx_y = [long]$b',
|
|
747
|
+
' switch ($op) {',
|
|
748
|
+
" '-eq' { return ($fx_x -eq $fx_y) }",
|
|
749
|
+
" '-ne' { return ($fx_x -ne $fx_y) }",
|
|
750
|
+
" '-lt' { return ($fx_x -lt $fx_y) }",
|
|
751
|
+
" '-le' { return ($fx_x -le $fx_y) }",
|
|
752
|
+
" '-gt' { return ($fx_x -gt $fx_y) }",
|
|
753
|
+
" '-ge' { return ($fx_x -ge $fx_y) }",
|
|
754
|
+
' }',
|
|
755
|
+
' return $false',
|
|
756
|
+
'}',
|
|
757
|
+
].join('\n');
|
|
758
|
+
function strNe(e) {
|
|
759
|
+
return "([string](" + e + ") -ne '')";
|
|
760
|
+
}
|
|
761
|
+
function testUnaryExpr(op, w) {
|
|
762
|
+
switch (op) {
|
|
763
|
+
case '-e':
|
|
764
|
+
case '-r':
|
|
765
|
+
case '-w':
|
|
766
|
+
case '-x':
|
|
767
|
+
return '(Test-Path -LiteralPath ' + operandExpr(w) + ')';
|
|
768
|
+
case '-f':
|
|
769
|
+
return '(Test-Path -LiteralPath ' + operandExpr(w) + ' -PathType Leaf)';
|
|
770
|
+
case '-d':
|
|
771
|
+
return '(Test-Path -LiteralPath ' + operandExpr(w) + ' -PathType Container)';
|
|
772
|
+
case '-s':
|
|
773
|
+
return ('((Test-Path -LiteralPath ' +
|
|
774
|
+
operandExpr(w) +
|
|
775
|
+
' -PathType Leaf) -and ((Get-Item -LiteralPath ' +
|
|
776
|
+
operandExpr(w) +
|
|
777
|
+
' -Force).Length -gt 0))');
|
|
778
|
+
case '-z':
|
|
779
|
+
return "([string](" + exprOfWord(w) + ") -eq '')";
|
|
780
|
+
case '-n':
|
|
781
|
+
default:
|
|
782
|
+
return strNe(exprOfWord(w));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function testBinaryExpr(l, op, r) {
|
|
786
|
+
const le = '[string](' + exprOfWord(l) + ')';
|
|
787
|
+
const re = '[string](' + exprOfWord(r) + ')';
|
|
788
|
+
if (op === '=' || op === '==')
|
|
789
|
+
return '(' + le + ' -ceq ' + re + ')';
|
|
790
|
+
if (op === '!=')
|
|
791
|
+
return '(' + le + ' -cne ' + re + ')';
|
|
792
|
+
return '(fx-tn (' + le + ') (' + re + ') ' + psStr(op) + ')';
|
|
793
|
+
}
|
|
794
|
+
function parseTestOr(ws, st) {
|
|
795
|
+
const r = parseTestAnd(ws, st);
|
|
796
|
+
if (r.error)
|
|
797
|
+
return r;
|
|
798
|
+
let expr = r.expr;
|
|
799
|
+
while (st.i < ws.length && wordToString(ws[st.i]) === '-o') {
|
|
800
|
+
st.i++;
|
|
801
|
+
const rr = parseTestAnd(ws, st);
|
|
802
|
+
if (rr.error)
|
|
803
|
+
return rr;
|
|
804
|
+
expr = '(' + expr + ') -or (' + rr.expr + ')';
|
|
805
|
+
}
|
|
806
|
+
return { expr, error: null };
|
|
807
|
+
}
|
|
808
|
+
function parseTestAnd(ws, st) {
|
|
809
|
+
const r = parseTestNot(ws, st);
|
|
810
|
+
if (r.error)
|
|
811
|
+
return r;
|
|
812
|
+
let expr = r.expr;
|
|
813
|
+
while (st.i < ws.length && wordToString(ws[st.i]) === '-a') {
|
|
814
|
+
st.i++;
|
|
815
|
+
const rr = parseTestNot(ws, st);
|
|
816
|
+
if (rr.error)
|
|
817
|
+
return rr;
|
|
818
|
+
expr = '(' + expr + ') -and (' + rr.expr + ')';
|
|
819
|
+
}
|
|
820
|
+
return { expr, error: null };
|
|
821
|
+
}
|
|
822
|
+
function parseTestNot(ws, st) {
|
|
823
|
+
const t = st.i < ws.length ? wordToString(ws[st.i]) : null;
|
|
824
|
+
if (t === '!' && st.i + 1 < ws.length) {
|
|
825
|
+
st.i++;
|
|
826
|
+
const r = parseTestNot(ws, st);
|
|
827
|
+
if (r.error)
|
|
828
|
+
return r;
|
|
829
|
+
return { expr: '(-not (' + r.expr + '))', error: null };
|
|
830
|
+
}
|
|
831
|
+
return parseTestAtom(ws, st);
|
|
832
|
+
}
|
|
833
|
+
function parseTestAtom(ws, st) {
|
|
834
|
+
if (st.i >= ws.length)
|
|
835
|
+
return { expr: null, error: 'too many arguments' };
|
|
836
|
+
const t = wordToString(ws[st.i]);
|
|
837
|
+
if (st.i === ws.length - 1) {
|
|
838
|
+
// single remaining word: bash test treats any non-null string as true
|
|
839
|
+
const e = exprOfWord(ws[st.i]);
|
|
840
|
+
st.i++;
|
|
841
|
+
return { expr: strNe(e), error: null };
|
|
842
|
+
}
|
|
843
|
+
if (TEST_UNARY.has(t)) {
|
|
844
|
+
const w = ws[st.i + 1];
|
|
845
|
+
st.i += 2;
|
|
846
|
+
return { expr: testUnaryExpr(t, w), error: null };
|
|
847
|
+
}
|
|
848
|
+
const nt = wordToString(ws[st.i + 1]);
|
|
849
|
+
if (TEST_BINARY.has(nt)) {
|
|
850
|
+
if (st.i + 2 < ws.length) {
|
|
851
|
+
const expr = testBinaryExpr(ws[st.i], nt, ws[st.i + 2]);
|
|
852
|
+
st.i += 3;
|
|
853
|
+
return { expr, error: null };
|
|
854
|
+
}
|
|
855
|
+
return { expr: null, error: 'OP:' + nt };
|
|
856
|
+
}
|
|
857
|
+
const e = exprOfWord(ws[st.i]);
|
|
858
|
+
st.i++;
|
|
859
|
+
return { expr: strNe(e), error: null };
|
|
860
|
+
}
|
|
861
|
+
function buildTest(ws, label) {
|
|
862
|
+
if (ws.length === 0)
|
|
863
|
+
return '$script:fx_exit = 1';
|
|
864
|
+
const st = { i: 0 };
|
|
865
|
+
const res = parseTestOr(ws, st);
|
|
866
|
+
let err = null;
|
|
867
|
+
if (res.error) {
|
|
868
|
+
err = res.error.startsWith('OP:')
|
|
869
|
+
? 'bash: ' + label + ': ' + res.error.slice(3) + ': binary operator expected'
|
|
870
|
+
: 'bash: ' + label + ': ' + res.error;
|
|
871
|
+
}
|
|
872
|
+
else if (st.i < ws.length) {
|
|
873
|
+
err = 'bash: ' + label + ': too many arguments';
|
|
874
|
+
}
|
|
875
|
+
if (err !== null) {
|
|
876
|
+
return '[Console]::Error.WriteLine(' + psStr(err) + '); $script:fx_exit = 2';
|
|
877
|
+
}
|
|
878
|
+
return [
|
|
879
|
+
FX_TN_FN,
|
|
880
|
+
'$fx_tr = ' + res.expr,
|
|
881
|
+
'if ($script:fx_exit -eq 2) { }',
|
|
882
|
+
'elseif (-not $fx_tr) { $script:fx_exit = 1 }',
|
|
883
|
+
].join('\n');
|
|
884
|
+
}
|
|
885
|
+
const test = (args) => buildTest(args, 'test');
|
|
886
|
+
const bracket = (args) => {
|
|
887
|
+
if (args.length === 0 || wordToString(args[args.length - 1]) !== ']') {
|
|
888
|
+
return '[Console]::Error.WriteLine(' + psStr("bash: [: missing `]'") + '); $script:fx_exit = 2';
|
|
889
|
+
}
|
|
890
|
+
return buildTest(args.slice(0, -1), '[');
|
|
891
|
+
};
|
|
892
|
+
/* ------------------------------------------------------------------ */
|
|
893
|
+
/* pushd / popd / dirs */
|
|
894
|
+
/* ------------------------------------------------------------------ */
|
|
895
|
+
/**
|
|
896
|
+
* The directory stack lives in $env:FAUXNIX_DIRSTACK (entries BELOW the
|
|
897
|
+
* current directory, ';'-joined) so it persists across fauxnix invocations
|
|
898
|
+
* through the executor's env channel.
|
|
899
|
+
*/
|
|
900
|
+
const DIRS_STACK_PS = [
|
|
901
|
+
'$fx_st = @()',
|
|
902
|
+
"if ($env:FAUXNIX_DIRSTACK) { $fx_st = @($env:FAUXNIX_DIRSTACK -split ';' | Where-Object { $_ -ne '' }) }",
|
|
903
|
+
].join('\n');
|
|
904
|
+
const DIRS_PRINT_PS = [
|
|
905
|
+
'$fx_l = @((Get-Location).Path)',
|
|
906
|
+
'foreach ($fx_s in $fx_st) { $fx_l += $fx_s }',
|
|
907
|
+
"($fx_l -join ' ').Replace('\\', '/')",
|
|
908
|
+
].join('\n');
|
|
909
|
+
const dirs = () => {
|
|
910
|
+
return [DIRS_STACK_PS, DIRS_PRINT_PS].join('\n');
|
|
911
|
+
};
|
|
912
|
+
const pushd = (args) => {
|
|
913
|
+
const operands = stripFlags(args);
|
|
914
|
+
if (operands.length === 0) {
|
|
915
|
+
// bash bare pushd: swap the top two entries
|
|
916
|
+
return [
|
|
917
|
+
DIRS_STACK_PS,
|
|
918
|
+
"if ($fx_st.Count -eq 0) { [Console]::Error.WriteLine('bash: pushd: no other directory'); $script:fx_exit = 1 }",
|
|
919
|
+
'else {',
|
|
920
|
+
' $fx_old = (Get-Location).Path',
|
|
921
|
+
' try { Set-Location -LiteralPath $fx_st[0] } catch { [Console]::Error.WriteLine("bash: pushd: " + $fx_st[0] + ": No such file or directory"); $script:fx_exit = 1 }',
|
|
922
|
+
' $fx_new = @($fx_old) + @($fx_st | Select-Object -Skip 1)',
|
|
923
|
+
" $env:FAUXNIX_DIRSTACK = ($fx_new -join ';')",
|
|
924
|
+
' if ($script:fx_exit -eq 0) { $fx_st = $fx_new; ' + DIRS_PRINT_PS + ' }',
|
|
925
|
+
'}',
|
|
926
|
+
].join('\n');
|
|
927
|
+
}
|
|
928
|
+
if (operands.length > 1) {
|
|
929
|
+
return "[Console]::Error.WriteLine('bash: pushd: too many arguments'); $script:fx_exit = 1";
|
|
930
|
+
}
|
|
931
|
+
return [
|
|
932
|
+
DIRS_STACK_PS,
|
|
933
|
+
'$fx_d = ' + operandExpr(operands[0]),
|
|
934
|
+
'if (-not (Test-Path -LiteralPath $fx_d -PathType Container)) { [Console]::Error.WriteLine("bash: pushd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 }',
|
|
935
|
+
'else {',
|
|
936
|
+
' $fx_old = (Get-Location).Path',
|
|
937
|
+
' try { Set-Location -LiteralPath $fx_d } catch { [Console]::Error.WriteLine("bash: pushd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 }',
|
|
938
|
+
' $fx_new = @($fx_old) + @($fx_st)',
|
|
939
|
+
" $env:FAUXNIX_DIRSTACK = ($fx_new -join ';')",
|
|
940
|
+
' if ($script:fx_exit -eq 0) { $fx_st = $fx_new; ' + DIRS_PRINT_PS + ' }',
|
|
941
|
+
'}',
|
|
942
|
+
].join('\n');
|
|
943
|
+
};
|
|
944
|
+
const popd = () => {
|
|
945
|
+
return [
|
|
946
|
+
DIRS_STACK_PS,
|
|
947
|
+
"if ($fx_st.Count -eq 0) { [Console]::Error.WriteLine('bash: popd: directory stack empty'); $script:fx_exit = 1 }",
|
|
948
|
+
'else {',
|
|
949
|
+
' $fx_t = $fx_st[0]',
|
|
950
|
+
' $fx_rest = @($fx_st | Select-Object -Skip 1)',
|
|
951
|
+
' if (-not (Test-Path -LiteralPath $fx_t -PathType Container)) { [Console]::Error.WriteLine("bash: popd: " + $fx_t + ": No such file or directory"); $script:fx_exit = 1 }',
|
|
952
|
+
' else {',
|
|
953
|
+
' try { Set-Location -LiteralPath $fx_t } catch { [Console]::Error.WriteLine("bash: popd: " + $fx_t + ": No such file or directory"); $script:fx_exit = 1 }',
|
|
954
|
+
" $env:FAUXNIX_DIRSTACK = ($fx_rest -join ';')",
|
|
955
|
+
' if ($script:fx_exit -eq 0) { $fx_st = $fx_rest; ' + DIRS_PRINT_PS + ' }',
|
|
956
|
+
' }',
|
|
957
|
+
'}',
|
|
958
|
+
].join('\n');
|
|
959
|
+
};
|
|
960
|
+
/* ------------------------------------------------------------------ */
|
|
961
|
+
/* sudo / timeout / man / history / less / more */
|
|
962
|
+
/* ------------------------------------------------------------------ */
|
|
963
|
+
const sudo = () => {
|
|
964
|
+
return ('[Console]::Error.WriteLine(' +
|
|
965
|
+
psStr('sudo: fauxnix cannot elevate privileges; run your agent from an elevated shell if the operation needs it') +
|
|
966
|
+
'); $script:fx_exit = 1');
|
|
967
|
+
};
|
|
968
|
+
const timeout = (args, ctx) => {
|
|
969
|
+
let i = 0;
|
|
970
|
+
const raw = args.map(wordToString);
|
|
971
|
+
while (i < raw.length && raw[i].startsWith('-') && raw[i] !== '-' && raw[i] !== '--')
|
|
972
|
+
i++;
|
|
973
|
+
if (i < raw.length && raw[i] === '--')
|
|
974
|
+
i++;
|
|
975
|
+
if (i >= args.length) {
|
|
976
|
+
return "[Console]::Error.WriteLine('timeout: missing operand'); $script:fx_exit = 125";
|
|
977
|
+
}
|
|
978
|
+
const nWord = args[i];
|
|
979
|
+
const cmdWords = args.slice(i + 1);
|
|
980
|
+
if (cmdWords.length === 0) {
|
|
981
|
+
return "[Console]::Error.WriteLine('timeout: missing operand'); $script:fx_exit = 125";
|
|
982
|
+
}
|
|
983
|
+
// NOTE: stdin is not forwarded into the job — documented limitation.
|
|
984
|
+
const inner = translateSimple({ kind: 'SimpleCommand', assignments: [], name: cmdWords[0], args: cmdWords.slice(1), redirects: [] }, ctx.position, false);
|
|
985
|
+
const innerLines = inner.split('\n').map((l) => (l ? ' ' + l : l));
|
|
986
|
+
return [
|
|
987
|
+
'$fx_tn = ' + exprOfWord(nWord),
|
|
988
|
+
"if ($fx_tn -notmatch '^[0-9]*\\.?[0-9]+[smhd]?$') { [Console]::Error.WriteLine(\"timeout: invalid time interval '\" + $fx_tn + \"'\"); $script:fx_exit = 125 }",
|
|
989
|
+
'else {',
|
|
990
|
+
" $fx_secs = [double]($fx_tn -replace '[smhd]$', '')",
|
|
991
|
+
" if ($fx_tn -match 'm$') { $fx_secs = $fx_secs * 60 } elseif ($fx_tn -match 'h$') { $fx_secs = $fx_secs * 3600 } elseif ($fx_tn -match 'd$') { $fx_secs = $fx_secs * 86400 }",
|
|
992
|
+
' $fx_sb = {',
|
|
993
|
+
' $script:fx_exit = 0',
|
|
994
|
+
...innerLines,
|
|
995
|
+
" '__FAUXNIX_EXIT__:' + [string]$script:fx_exit",
|
|
996
|
+
' }',
|
|
997
|
+
' $fx_j = Start-Job -ScriptBlock $fx_sb',
|
|
998
|
+
' $fx_deadline = (Get-Date).AddSeconds($fx_secs)',
|
|
999
|
+
' $fx_done = $false',
|
|
1000
|
+
' while ((Get-Date) -lt $fx_deadline) {',
|
|
1001
|
+
" if ((Get-Job -Id $fx_j.Id).State -ne 'Running') { $fx_done = $true; break }",
|
|
1002
|
+
' Start-Sleep -Milliseconds 100',
|
|
1003
|
+
' }',
|
|
1004
|
+
' if (-not $fx_done) {',
|
|
1005
|
+
' Stop-Job $fx_j',
|
|
1006
|
+
" [Console]::Error.WriteLine('timeout: fauxnix timed out after ' + $fx_tn + 's')",
|
|
1007
|
+
' $script:fx_exit = 124',
|
|
1008
|
+
' } else {',
|
|
1009
|
+
' $fx_out = @(Receive-Job $fx_j)',
|
|
1010
|
+
' foreach ($fx_e in @($fx_j.ChildJobs[0].Error | ForEach-Object { [string]$_ })) { [Console]::Error.WriteLine($fx_e) }',
|
|
1011
|
+
' if ($fx_out.Count -gt 0) {',
|
|
1012
|
+
' $fx_last = [string]$fx_out[$fx_out.Count - 1]',
|
|
1013
|
+
" if ($fx_last -like '__FAUXNIX_EXIT__:*') {",
|
|
1014
|
+
' for ($fx_k = 0; $fx_k -lt ($fx_out.Count - 1); $fx_k++) { [string]$fx_out[$fx_k] }',
|
|
1015
|
+
" $fx_c = 0",
|
|
1016
|
+
" $fx_cv = $fx_last -replace '^__FAUXNIX_EXIT__:', ''",
|
|
1017
|
+
" try { $fx_c = [int]$fx_cv } catch { $fx_c = 1 }",
|
|
1018
|
+
' $script:fx_exit = $fx_c',
|
|
1019
|
+
' } else {',
|
|
1020
|
+
' foreach ($fx_o in $fx_out) { [string]$fx_o }',
|
|
1021
|
+
' }',
|
|
1022
|
+
' }',
|
|
1023
|
+
' }',
|
|
1024
|
+
" Remove-Job $fx_j -Force -ErrorAction SilentlyContinue",
|
|
1025
|
+
'}',
|
|
1026
|
+
].join('\n');
|
|
1027
|
+
};
|
|
1028
|
+
const man = (args) => {
|
|
1029
|
+
const w = stripFlags(args)[0];
|
|
1030
|
+
if (!w) {
|
|
1031
|
+
return "[Console]::Error.WriteLine('What manual page do you want?'); $script:fx_exit = 2";
|
|
1032
|
+
}
|
|
1033
|
+
return [
|
|
1034
|
+
'$fx_m = ' + exprOfWord(w),
|
|
1035
|
+
"[Console]::Error.WriteLine('No manual entry for ' + $fx_m)",
|
|
1036
|
+
'[Console]::Error.WriteLine(\'(fauxnix: try "\' + $fx_m + \' --help" or "\' + $fx_m + \' -h")\')',
|
|
1037
|
+
'$script:fx_exit = 1',
|
|
1038
|
+
].join('\n');
|
|
1039
|
+
};
|
|
1040
|
+
const history = () => '';
|
|
1041
|
+
/** less/more: paging is impossible for agents — behave exactly like cat. */
|
|
1042
|
+
const less = (args, ctx) => {
|
|
1043
|
+
const files = stripFlags(args);
|
|
1044
|
+
const cat = textIoHandlers['cat'];
|
|
1045
|
+
if (!cat)
|
|
1046
|
+
return '';
|
|
1047
|
+
return cat(files, ctx);
|
|
1048
|
+
};
|
|
1049
|
+
/* ------------------------------------------------------------------ */
|
|
1050
|
+
/* source / . / eval / exit / alias / set */
|
|
1051
|
+
/* ------------------------------------------------------------------ */
|
|
1052
|
+
const source = () => {
|
|
1053
|
+
return ('[Console]::Error.WriteLine(' +
|
|
1054
|
+
psStr('fauxnix: source/. requires a persistent shell; fauxnix persists cwd and env across calls but not shell functions') +
|
|
1055
|
+
'); $script:fx_exit = 1');
|
|
1056
|
+
};
|
|
1057
|
+
const evalCmd = () => {
|
|
1058
|
+
return ("[Console]::Error.WriteLine('fauxnix: eval is not supported; pass the command itself'); $script:fx_exit = 1");
|
|
1059
|
+
};
|
|
1060
|
+
const exitCmd = (args) => {
|
|
1061
|
+
const w = args[0];
|
|
1062
|
+
if (!w)
|
|
1063
|
+
return '$script:fx_exit = 0';
|
|
1064
|
+
const lit = wordToString(w);
|
|
1065
|
+
if (/^-?\d+$/.test(lit)) {
|
|
1066
|
+
let n = parseInt(lit, 10) % 256;
|
|
1067
|
+
if (n < 0)
|
|
1068
|
+
n += 256;
|
|
1069
|
+
return '$script:fx_exit = ' + n;
|
|
1070
|
+
}
|
|
1071
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(lit)) {
|
|
1072
|
+
// static non-numeric literal
|
|
1073
|
+
return ('[Console]::Error.WriteLine(' +
|
|
1074
|
+
psStr('bash: exit: ' + lit + ': numeric argument required') +
|
|
1075
|
+
'); $script:fx_exit = 2');
|
|
1076
|
+
}
|
|
1077
|
+
return [
|
|
1078
|
+
'$fx_e = ' + exprOfWord(w),
|
|
1079
|
+
"if ($fx_e -notmatch '^-?[0-9]+$') { [Console]::Error.WriteLine('bash: exit: ' + $fx_e + ': numeric argument required'); $script:fx_exit = 2 }",
|
|
1080
|
+
'else { $fx_n = [int]$fx_e % 256; if ($fx_n -lt 0) { $fx_n += 256 }; $script:fx_exit = $fx_n }',
|
|
1081
|
+
].join('\n');
|
|
1082
|
+
};
|
|
1083
|
+
const alias = (args) => {
|
|
1084
|
+
const has = args.some((w) => {
|
|
1085
|
+
const t = wordToString(w);
|
|
1086
|
+
return t !== '' && !t.startsWith('-');
|
|
1087
|
+
});
|
|
1088
|
+
if (!has)
|
|
1089
|
+
return '';
|
|
1090
|
+
return "[Console]::Error.WriteLine('fauxnix: alias is not supported'); $script:fx_exit = 1";
|
|
1091
|
+
};
|
|
1092
|
+
const set = () => ''; // silently ignore (`set -e`, `set --` ... no-op)
|
|
1093
|
+
/* ------------------------------------------------------------------ */
|
|
1094
|
+
export const handlers = {
|
|
1095
|
+
cd,
|
|
1096
|
+
pwd,
|
|
1097
|
+
export: exportCmd,
|
|
1098
|
+
unset,
|
|
1099
|
+
env,
|
|
1100
|
+
printenv,
|
|
1101
|
+
ps,
|
|
1102
|
+
kill,
|
|
1103
|
+
pkill,
|
|
1104
|
+
pgrep,
|
|
1105
|
+
sleep,
|
|
1106
|
+
which,
|
|
1107
|
+
type,
|
|
1108
|
+
whoami,
|
|
1109
|
+
id,
|
|
1110
|
+
groups,
|
|
1111
|
+
date,
|
|
1112
|
+
uname,
|
|
1113
|
+
hostname,
|
|
1114
|
+
uptime,
|
|
1115
|
+
free,
|
|
1116
|
+
nproc,
|
|
1117
|
+
clear,
|
|
1118
|
+
true: trueCmd,
|
|
1119
|
+
false: falseCmd,
|
|
1120
|
+
test,
|
|
1121
|
+
'[': bracket,
|
|
1122
|
+
':': colon,
|
|
1123
|
+
pushd,
|
|
1124
|
+
popd,
|
|
1125
|
+
dirs,
|
|
1126
|
+
sudo,
|
|
1127
|
+
timeout,
|
|
1128
|
+
man,
|
|
1129
|
+
history,
|
|
1130
|
+
less,
|
|
1131
|
+
more: less,
|
|
1132
|
+
source,
|
|
1133
|
+
'.': source,
|
|
1134
|
+
eval: evalCmd,
|
|
1135
|
+
exit: exitCmd,
|
|
1136
|
+
alias,
|
|
1137
|
+
set,
|
|
1138
|
+
};
|