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.
@@ -0,0 +1,2281 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { FauxnixParseError, wordToString } from '../ast.js';
5
+ import { parseWords, psStr } from '../registry.js';
6
+ import { exprOfWord, literalOfWord, operandExpr } from '../translator.js';
7
+ /* ------------------------------------------------------------------ */
8
+ /* Shared PS snippets (same shape as files.ts) */
9
+ /* ------------------------------------------------------------------ */
10
+ const PS_GLOB_FN = [
11
+ 'function fx-glob($p) {',
12
+ " if ($p -notlike '*[*?]*') { return @($p) }",
13
+ ' $m = @(Get-Item -Path $p -ErrorAction SilentlyContinue)',
14
+ ' if ($m.Count -eq 0) { return @($p) }',
15
+ ' return @($m | ForEach-Object { $_.FullName })',
16
+ '}',
17
+ ].join('\n');
18
+ const PS_READTEXT_FN = [
19
+ 'function fx-read($p) {',
20
+ ' $b = [IO.File]::ReadAllBytes($p)',
21
+ ' try { return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) }',
22
+ ' catch { try { return [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { return [System.Text.Encoding]::ASCII.GetString($b) } }',
23
+ '}',
24
+ ].join('\n');
25
+ /** Split text into lines, GNU-style (trailing newline makes no extra line). */
26
+ const PS_SPLITLINES_FN = [
27
+ 'function fx-splitlines($t) {',
28
+ ' $t = $t.Replace([string][char]13 + [string][char]10, [string][char]10).Replace([string][char]13, [string][char]10)',
29
+ " if ($t -eq '') { return @() }",
30
+ ' if ($t.EndsWith([string][char]10)) { $t = $t.Substring(0, $t.Length - 1) }',
31
+ ' return @($t.Split([char]10))',
32
+ '}',
33
+ ].join('\n');
34
+ const STDIN_LINES = '@($input | ForEach-Object { [string]$_ })';
35
+ /** Operand Words → PS array expression of string exprs. */
36
+ function psArray(words, fn = operandExpr) {
37
+ if (words.length === 0)
38
+ return '@()';
39
+ return '@(' + words.map(fn).join(', ') + ')';
40
+ }
41
+ /** PS boolean literal. */
42
+ function pb(v) {
43
+ return v ? '$true' : '$false';
44
+ }
45
+ /**
46
+ * Like psStr but flattens embedded control characters (\n \t \r) into
47
+ * [char]N concatenations — a raw newline inside a PS string literal would be
48
+ * re-indented (and corrupted) by the executor's wrapper.
49
+ */
50
+ function psStrFlat(s) {
51
+ const parts = [];
52
+ let lit = '';
53
+ for (const ch of s) {
54
+ if (ch === '\n' || ch === '\t' || ch === '\r') {
55
+ if (lit !== '') {
56
+ parts.push(psStr(lit));
57
+ lit = '';
58
+ }
59
+ parts.push('[string][char]' + (ch === '\n' ? 10 : ch === '\t' ? 9 : 13));
60
+ }
61
+ else {
62
+ lit += ch;
63
+ }
64
+ }
65
+ if (lit !== '')
66
+ parts.push(psStr(lit));
67
+ if (parts.length === 0)
68
+ return "''";
69
+ return '(' + parts.join(' + ') + ')';
70
+ }
71
+ /**
72
+ * A text argument (pattern, delimiter, script piece). Unlike operandExpr
73
+ * this NEVER applies path normalization — 'a/b' must stay 'a/b'.
74
+ */
75
+ function textExpr(w) {
76
+ const lit = literalOfWord(w);
77
+ if (lit !== null)
78
+ return psStr(lit);
79
+ return exprOfWord(w);
80
+ }
81
+ /** Collect EVERY value of a short option (-kN, -k N) — parseWords keeps only the last. */
82
+ function collectShortValues(args, letter, longName) {
83
+ const out = [];
84
+ let onlyOps = false;
85
+ for (let i = 0; i < args.length; i++) {
86
+ const t = wordToString(args[i]);
87
+ if (t === '--') {
88
+ onlyOps = true;
89
+ continue;
90
+ }
91
+ if (onlyOps)
92
+ continue;
93
+ if (longName && t.startsWith(longName + '='))
94
+ out.push(t.slice(longName.length + 1));
95
+ else if (t === '-' + letter) {
96
+ if (i + 1 < args.length) {
97
+ out.push(wordToString(args[i + 1]));
98
+ i++;
99
+ }
100
+ }
101
+ else if (t.startsWith('-' + letter) && t.length > 2 && !t.startsWith('--')) {
102
+ out.push(t.slice(2));
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ /** Build the "collect file operands through fx-glob" PS prologue. */
108
+ function psCollectSources(filesExpr, cmdErr, leafOnly) {
109
+ const test = leafOnly
110
+ ? '-not (Test-Path -LiteralPath $fx_g -PathType Leaf)'
111
+ : '-not (Test-Path -LiteralPath $fx_g)';
112
+ return [
113
+ '$fx_srcs = @()',
114
+ '$fx_err = $false',
115
+ 'foreach ($fx_o in ' + filesExpr + ') {',
116
+ ' foreach ($fx_g in (fx-glob $fx_o)) {',
117
+ ' if (' + test + ') { ' + cmdErr('$fx_g') + '; $fx_err = $true; continue }',
118
+ ' $fx_srcs += $fx_g',
119
+ ' }',
120
+ '}',
121
+ ];
122
+ }
123
+ /** Resolve a POSIX-ish literal path for node:fs (sed -f). */
124
+ function nodePathOf(p) {
125
+ if (p === '/tmp')
126
+ return os.tmpdir();
127
+ if (p.startsWith('/tmp/'))
128
+ return path.join(os.tmpdir(), p.slice(5));
129
+ const m = p.match(/^\/([a-zA-Z])\/(.*)$/);
130
+ if (m)
131
+ return m[1].toUpperCase() + ':\\' + m[2].split('/').join('\\');
132
+ return p;
133
+ }
134
+ /* ------------------------------------------------------------------ */
135
+ /* BRE / ERE → .NET regex translation */
136
+ /* ------------------------------------------------------------------ */
137
+ const POSIX_CLASSES = {
138
+ alpha: 'A-Za-z',
139
+ digit: '0-9',
140
+ lower: 'a-z',
141
+ upper: 'A-Z',
142
+ alnum: 'A-Za-z0-9',
143
+ space: '\\s',
144
+ blank: ' \\t',
145
+ xdigit: '0-9A-Fa-f',
146
+ punct: '!-/:-@[-`{-~',
147
+ cntrl: '\\x00-\\x1F',
148
+ print: ' -~',
149
+ graph: '!-~',
150
+ };
151
+ /** Replace [:class:] inside a pattern (only meaningful inside brackets). */
152
+ function posixClassFix(re) {
153
+ return re.replace(/\[:([a-z]+):\]/g, (m, name) => {
154
+ const rep = POSIX_CLASSES[name];
155
+ if (rep === undefined)
156
+ return m;
157
+ return rep;
158
+ });
159
+ }
160
+ /** POSIX BRE → .NET regex (best-effort). */
161
+ function breToDotNet(re) {
162
+ let out = '';
163
+ let i = 0;
164
+ while (i < re.length) {
165
+ const c = re[i];
166
+ if (c === '\\') {
167
+ const n = re[i + 1];
168
+ if (n === undefined) {
169
+ out += '\\\\';
170
+ i++;
171
+ }
172
+ else if ('(){}|+?'.includes(n)) {
173
+ out += n;
174
+ i += 2;
175
+ }
176
+ else {
177
+ out += '\\' + n;
178
+ i += 2;
179
+ }
180
+ continue;
181
+ }
182
+ if (c === '[') {
183
+ let j = i + 1;
184
+ let cls = '[';
185
+ if (re[j] === '^') {
186
+ cls += '^';
187
+ j++;
188
+ }
189
+ if (re[j] === ']') {
190
+ cls += ']';
191
+ j++;
192
+ }
193
+ while (j < re.length && re[j] !== ']') {
194
+ cls += re[j];
195
+ j++;
196
+ }
197
+ if (j < re.length) {
198
+ cls += ']';
199
+ j++;
200
+ }
201
+ out += posixClassFix(cls);
202
+ i = j;
203
+ continue;
204
+ }
205
+ if ('()|+?'.includes(c)) {
206
+ out += '\\' + c;
207
+ i++;
208
+ continue;
209
+ }
210
+ if (c === '{' || c === '}') {
211
+ out += '\\' + c;
212
+ i++;
213
+ continue;
214
+ }
215
+ if (c === '*' && i === 0) {
216
+ out += '\\*';
217
+ i++;
218
+ continue;
219
+ }
220
+ if (c === '^') {
221
+ out += i === 0 ? '^' : '\\^';
222
+ i++;
223
+ continue;
224
+ }
225
+ if (c === '$' && i !== re.length - 1) {
226
+ out += '\\$';
227
+ i++;
228
+ continue;
229
+ }
230
+ out += c;
231
+ i++;
232
+ }
233
+ return out;
234
+ }
235
+ /** POSIX ERE → .NET regex (close to identity; fix character classes). */
236
+ function ereToDotNet(re) {
237
+ return posixClassFix(re);
238
+ }
239
+ /* ------------------------------------------------------------------ */
240
+ /* grep */
241
+ /* ------------------------------------------------------------------ */
242
+ const grep = (args) => {
243
+ const includeGlobs = collectShortValues(args, '', '--include');
244
+ const { flags, operandWords, values } = parseWords(args, ['A', 'B', 'C']);
245
+ const ci = flags.has('i');
246
+ const inv = flags.has('v');
247
+ const num = flags.has('n');
248
+ const cntMode = flags.has('c');
249
+ const listMode = flags.has('l');
250
+ const rec = flags.has('r') || flags.has('R');
251
+ const ere = flags.has('E');
252
+ const fixed = flags.has('F');
253
+ const word = flags.has('w');
254
+ const quiet = flags.has('q');
255
+ const onlyMatch = flags.has('o');
256
+ const suppressFname = flags.has('h');
257
+ const forceFname = flags.has('H');
258
+ const toInt = (s) => {
259
+ const n = s === undefined ? NaN : parseInt(s, 10);
260
+ return Number.isFinite(n) && n > 0 ? n : 0;
261
+ };
262
+ const ctxA = Math.max(toInt(values.get('-A')), toInt(values.get('-C')));
263
+ const ctxB = Math.max(toInt(values.get('-B')), toInt(values.get('-C')));
264
+ if (operandWords.length === 0) {
265
+ return ("[Console]::Error.WriteLine('usage: grep [OPTION]... PATTERN [FILE]...'); $script:fx_exit = 2");
266
+ }
267
+ const patternWord = operandWords[0];
268
+ const fileWords = operandWords.slice(1);
269
+ const patLit = literalOfWord(patternWord);
270
+ let patExpr;
271
+ if (fixed || patLit === null) {
272
+ patExpr = textExpr(patternWord);
273
+ }
274
+ else {
275
+ patExpr = psStr(ere ? ereToDotNet(patLit) : breToDotNet(patLit));
276
+ }
277
+ const lines = [PS_READTEXT_FN, PS_SPLITLINES_FN];
278
+ // --- pattern objects -------------------------------------------------
279
+ if (fixed) {
280
+ lines.push('$fx_needle = ' + patExpr);
281
+ if (ci)
282
+ lines.push('$fx_needle_ll = $fx_needle.ToLower()');
283
+ }
284
+ else {
285
+ lines.push('$fx_pat = ' + patExpr);
286
+ if (word)
287
+ lines.push("$fx_pat = '(?<!\\w)(?:' + $fx_pat + ')(?!\\w)'");
288
+ lines.push(ci
289
+ ? '$fx_re = New-Object System.Text.RegularExpressions.Regex($fx_pat, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)'
290
+ : '$fx_re = New-Object System.Text.RegularExpressions.Regex($fx_pat)');
291
+ }
292
+ // --- fx-gmatch: line test (-v applied) -------------------------------
293
+ lines.push('function fx-gmatch($l) {');
294
+ if (fixed) {
295
+ if (word) {
296
+ if (ci)
297
+ lines.push(' $lx = $l.ToLower()');
298
+ const hay = ci ? '$lx' : '$l';
299
+ const needle = ci ? '$fx_needle_ll' : '$fx_needle';
300
+ lines.push(' $p = ' + hay + '.IndexOf(' + needle + ')');
301
+ lines.push(' while ($p -ge 0) {');
302
+ lines.push(' $ok = $true');
303
+ lines.push(" if ($p -gt 0) { $c = " + hay + "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
304
+ lines.push(' if ($ok) { $e = $p + ' + needle + '.Length; if ($e -lt ' + hay + '.Length) { $c = ' + hay + "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
305
+ lines.push(' if ($ok) { return ' + pb(!inv) + ' }');
306
+ lines.push(' $p = ' + hay + '.IndexOf(' + needle + ', $p + 1)');
307
+ lines.push(' }');
308
+ lines.push(' return ' + pb(inv));
309
+ }
310
+ else {
311
+ const hay = ci ? '$l.ToLower()' : '$l';
312
+ const needle = ci ? '$fx_needle_ll' : '$fx_needle';
313
+ const hit = hay + '.Contains(' + needle + ')';
314
+ lines.push(' return ' + (inv ? '-not (' + hit + ')' : hit));
315
+ }
316
+ }
317
+ else {
318
+ const hit = '$fx_re.IsMatch($l)';
319
+ lines.push(' return ' + (inv ? '-not (' + hit + ')' : hit));
320
+ }
321
+ lines.push('}');
322
+ // --- source collection -------------------------------------------------
323
+ if (fileWords.length > 0) {
324
+ lines.push(PS_GLOB_FN);
325
+ lines.push('$fx_inc = @(' + includeGlobs.map((g) => psStr(g)).join(', ') + ')', '$fx_srcs = @()', '$fx_err = $false', '$fx_recd = $false');
326
+ lines.push('foreach ($fx_o in ' + psArray(fileWords) + ') {');
327
+ lines.push(' foreach ($fx_g in (fx-glob $fx_o)) {');
328
+ lines.push(" if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine('grep: ' + $fx_g + ': No such file or directory'); $fx_err = $true; continue }");
329
+ lines.push(' if (Test-Path -LiteralPath $fx_g -PathType Container) {');
330
+ if (rec) {
331
+ lines.push(' $fx_recd = $true');
332
+ lines.push(' $fx_subs = @(Get-ChildItem -LiteralPath $fx_g -Recurse -Force -File -ErrorAction SilentlyContinue)');
333
+ lines.push(' if ($fx_inc.Count -gt 0) {');
334
+ lines.push(" $fx_subs = @($fx_subs | Where-Object { $fx_ok = $false; foreach ($fx_gi in $fx_inc) { if ($_.Name -like $fx_gi) { $fx_ok = $true; break } }; $fx_ok })");
335
+ lines.push(' }');
336
+ lines.push(' foreach ($fx_s in $fx_subs) { $fx_srcs += $fx_s.FullName }');
337
+ }
338
+ else {
339
+ lines.push(" [Console]::Error.WriteLine('grep: ' + $fx_g + ': Is a directory'); $fx_err = $true");
340
+ }
341
+ lines.push(' } else { $fx_srcs += $fx_g }');
342
+ lines.push(' }');
343
+ lines.push('}');
344
+ lines.push('$fx_pre = $false');
345
+ lines.push('if (-not ' +
346
+ pb(suppressFname) +
347
+ ') { if (' +
348
+ pb(forceFname) +
349
+ ' -or $fx_srcs.Count -gt 1 -or $fx_recd) { $fx_pre = $true } }');
350
+ }
351
+ // --- line emit helper (normal / context modes) -------------------------
352
+ lines.push('function fx-emitline($i, $s) {');
353
+ if (num)
354
+ lines.push(" $s = ([string]($i + 1)) + ':' + $s");
355
+ lines.push(" if ($fx_pre) { $s = $fx_disp + ':' + $s }");
356
+ lines.push(' $s');
357
+ lines.push('}');
358
+ // --- per-source scan body ----------------------------------------------
359
+ const scan = [];
360
+ if (cntMode) {
361
+ scan.push('$fx_c = 0');
362
+ scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
363
+ scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_c++ }');
364
+ scan.push('}');
365
+ scan.push('if ($fx_c -gt 0) { $fx_any = $true }');
366
+ scan.push('if ($fx_pre) { $fx_disp + \':\' + [string]$fx_c } else { [string]$fx_c }');
367
+ }
368
+ else if (listMode) {
369
+ scan.push('$fx_hit1 = $false');
370
+ scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
371
+ scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_hit1 = $true; break }');
372
+ scan.push('}');
373
+ scan.push('if ($fx_hit1) { $fx_any = $true; $fx_disp }');
374
+ }
375
+ else if (quiet) {
376
+ scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
377
+ scan.push(' if (fx-gmatch $fx_ls[$fx_i]) { $fx_any = $true; break }');
378
+ scan.push('}');
379
+ }
380
+ else {
381
+ scan.push('$fx_hits = @()');
382
+ scan.push('for ($fx_i = 0; $fx_i -lt $fx_ls.Count; $fx_i++) {');
383
+ scan.push(' $fx_l = $fx_ls[$fx_i]');
384
+ scan.push(' if (fx-gmatch $fx_l) {');
385
+ scan.push(' $fx_any = $true');
386
+ if (onlyMatch && !inv) {
387
+ if (fixed) {
388
+ if (ci) {
389
+ scan.push(' $lx = $fx_l.ToLower()');
390
+ scan.push(' $p = $lx.IndexOf($fx_needle_ll)');
391
+ }
392
+ else {
393
+ scan.push(' $p = $fx_l.IndexOf($fx_needle)');
394
+ }
395
+ const hay = ci ? '$lx' : '$fx_l';
396
+ const needle = ci ? '$fx_needle_ll' : '$fx_needle';
397
+ scan.push(' while ($p -ge 0) {');
398
+ scan.push(' $ok = $true');
399
+ if (word) {
400
+ scan.push(" if ($p -gt 0) { $c = " + hay + "[$p - 1]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') }");
401
+ scan.push(' if ($ok) { $e = $p + ' + needle + '.Length; if ($e -lt ' + hay + '.Length) { $c = ' + hay + "[$e]; $ok = -not ([char]::IsLetterOrDigit($c) -or $c -eq '_') } }");
402
+ scan.push(' if ($ok) { fx-emitline $fx_i (' + hay + '.Substring($p, ' + needle + '.Length)) }');
403
+ }
404
+ else {
405
+ scan.push(' fx-emitline $fx_i (' + hay + '.Substring($p, ' + needle + '.Length))');
406
+ }
407
+ scan.push(' $p = ' + hay + '.IndexOf(' + needle + ', $p + 1)');
408
+ scan.push(' }');
409
+ }
410
+ else {
411
+ scan.push(' foreach ($fx_m in $fx_re.Matches($fx_l)) { fx-emitline $fx_i $fx_m.Value }');
412
+ }
413
+ }
414
+ else if (!onlyMatch) {
415
+ scan.push(' $fx_hits += $fx_i');
416
+ }
417
+ scan.push(' }');
418
+ scan.push('}');
419
+ if (!onlyMatch) {
420
+ if (ctxA > 0 || ctxB > 0) {
421
+ scan.push('$fx_show = @()');
422
+ scan.push('foreach ($fx_h in $fx_hits) {');
423
+ scan.push(' $lo = $fx_h - ' + ctxB + '; if ($lo -lt 0) { $lo = 0 }');
424
+ scan.push(' $hi = $fx_h + ' + ctxA + '; if ($hi -gt $fx_ls.Count - 1) { $hi = $fx_ls.Count - 1 }');
425
+ scan.push(' $fx_show += ,@($lo, $hi)');
426
+ scan.push('}');
427
+ scan.push('$g0 = -1; $g1 = -1');
428
+ scan.push('foreach ($w in $fx_show) {');
429
+ scan.push(' if ($g0 -lt 0) { $g0 = $w[0]; $g1 = $w[1] }');
430
+ scan.push(' elseif ($w[0] -le $g1 + 1) { if ($w[1] -gt $g1) { $g1 = $w[1] } }');
431
+ scan.push(' else {');
432
+ scan.push(' for ($fx_j = $g0; $fx_j -le $g1; $fx_j++) { fx-emitline $fx_j $fx_ls[$fx_j] }');
433
+ scan.push(" '--'");
434
+ scan.push(' $g0 = $w[0]; $g1 = $w[1]');
435
+ scan.push(' }');
436
+ scan.push('}');
437
+ scan.push('if ($g0 -ge 0) { for ($fx_j = $g0; $fx_j -le $g1; $fx_j++) { fx-emitline $fx_j $fx_ls[$fx_j] } }');
438
+ }
439
+ else {
440
+ scan.push('foreach ($fx_h in $fx_hits) { fx-emitline $fx_h $fx_ls[$fx_h] }');
441
+ }
442
+ }
443
+ }
444
+ lines.push('$fx_any = $false');
445
+ if (fileWords.length > 0) {
446
+ lines.push('foreach ($fx_f in $fx_srcs) {');
447
+ lines.push(' $fx_ls = @(fx-splitlines (fx-read $fx_f))');
448
+ lines.push(' $fx_disp = $fx_f');
449
+ for (const l of scan)
450
+ lines.push(' ' + l);
451
+ lines.push('}');
452
+ }
453
+ else {
454
+ lines.push('$fx_pre = $false');
455
+ lines.push("$fx_disp = '(standard input)'");
456
+ lines.push('$fx_ls = ' + STDIN_LINES);
457
+ for (const l of scan)
458
+ lines.push(l);
459
+ }
460
+ lines.push('if ($fx_any) { $script:fx_exit = 0 } else { $script:fx_exit = 1 }');
461
+ if (!quiet)
462
+ lines.push('if ($fx_err) { $script:fx_exit = 2 }');
463
+ return lines.join('\n');
464
+ };
465
+ /** sed s-command RHS → .NET Regex substitution string. */
466
+ function sedReplToNet(repl, delim) {
467
+ let out = '';
468
+ let i = 0;
469
+ while (i < repl.length) {
470
+ const c = repl[i];
471
+ if (c === '\\' && i + 1 < repl.length) {
472
+ const n = repl[i + 1];
473
+ if (n === delim)
474
+ out += delim;
475
+ else if (n === '\\')
476
+ out += '\\';
477
+ else if (n === 'n')
478
+ out += '\n';
479
+ else if (n === 't')
480
+ out += '\t';
481
+ else if (n === '&')
482
+ out += '&';
483
+ else if (/[1-9]/.test(n))
484
+ out += '$' + n;
485
+ else
486
+ out += '\\' + n;
487
+ i += 2;
488
+ continue;
489
+ }
490
+ if (c === '&') {
491
+ out += '$&';
492
+ i++;
493
+ continue;
494
+ }
495
+ if (c === '$') {
496
+ out += '$$';
497
+ i++;
498
+ continue;
499
+ }
500
+ out += c;
501
+ i++;
502
+ }
503
+ return out;
504
+ }
505
+ function parseSedAddr(p, isEre) {
506
+ const s = p.s;
507
+ let i = p.i;
508
+ while (i < s.length && s[i] === ' ')
509
+ i++;
510
+ p.i = i;
511
+ if (i >= s.length)
512
+ return undefined;
513
+ const c = s[i];
514
+ if (c === '$' && s[i + 1] !== '!') {
515
+ p.i = i + 1;
516
+ return { k: 'last' };
517
+ }
518
+ if (c === '/' || (c === '\\' && s[i + 1] !== undefined && !/[a-zA-Z0-9]/.test(s[i + 1]))) {
519
+ const esc = c === '\\';
520
+ const delim = esc ? s[i + 1] : '/';
521
+ let j = esc ? i + 2 : i + 1;
522
+ let re = '';
523
+ while (j < s.length && s[j] !== delim) {
524
+ if (s[j] === '\\' && j + 1 < s.length) {
525
+ if (s[j + 1] === delim)
526
+ re += delim;
527
+ else
528
+ re += s[j] + s[j + 1];
529
+ j += 2;
530
+ continue;
531
+ }
532
+ re += s[j];
533
+ j++;
534
+ }
535
+ if (j >= s.length) {
536
+ throw new FauxnixParseError('fauxnix: sed unterminated regular expression');
537
+ }
538
+ j++;
539
+ let ci = false;
540
+ while (j < s.length && (s[j] === 'I' || s[j] === 'M')) {
541
+ if (s[j] === 'I')
542
+ ci = true;
543
+ j++;
544
+ }
545
+ p.i = j;
546
+ return { k: 're', re: isEre ? ereToDotNet(re) : breToDotNet(re), ci };
547
+ }
548
+ const dm = s.slice(i).match(/^(\d+)/);
549
+ if (dm) {
550
+ let n = parseInt(dm[1], 10);
551
+ i += dm[1].length;
552
+ if (s[i] === '~') {
553
+ const sm = s.slice(i + 1).match(/^(\d+)/);
554
+ if (!sm || parseInt(sm[1], 10) <= 0) {
555
+ throw new FauxnixParseError('fauxnix: sed invalid step address');
556
+ }
557
+ const step = parseInt(sm[1], 10);
558
+ if (n === 0)
559
+ n = step;
560
+ p.i = i + 1 + sm[1].length;
561
+ return { k: 'step', first: n, step };
562
+ }
563
+ p.i = i;
564
+ return { k: 'line', n };
565
+ }
566
+ return undefined;
567
+ }
568
+ const SED_UNSUPPORTED = {
569
+ '{': 'sed { } blocks',
570
+ '}': 'sed { } blocks',
571
+ ':': 'sed labels',
572
+ b: 'sed branches (b)',
573
+ t: 'sed branches (t)',
574
+ T: 'sed branches (T)',
575
+ h: 'sed hold space (h)',
576
+ H: 'sed hold space (H)',
577
+ g: 'sed hold space (g)',
578
+ G: 'sed hold space (G)',
579
+ x: 'sed hold space (x)',
580
+ n: 'sed multi-line (n)',
581
+ N: 'sed multi-line (N)',
582
+ P: 'sed multi-line (P)',
583
+ D: 'sed multi-line (D)',
584
+ w: 'sed write-to-file (w)',
585
+ W: 'sed write-to-file (W)',
586
+ r: 'sed read-file (r)',
587
+ R: 'sed read-file (R)',
588
+ a: 'sed append (a\\)',
589
+ i: 'sed insert (i\\)',
590
+ c: 'sed change (c\\)',
591
+ e: 'sed execute (e)',
592
+ F: 'sed filename (F)',
593
+ z: 'sed zap (z)',
594
+ l: 'sed list (l)',
595
+ v: 'sed version (v)',
596
+ L: 'sed line length (L)',
597
+ Q: 'sed quit-two (Q)',
598
+ };
599
+ function parseSedScript(src, isEre) {
600
+ const out = [];
601
+ const p = { s: src, i: 0 };
602
+ const s = src;
603
+ while (p.i < s.length) {
604
+ let i = p.i;
605
+ while (i < s.length && ' \t\r\n;'.includes(s[i]))
606
+ i++;
607
+ if (i >= s.length)
608
+ break;
609
+ if (s[i] === '#') {
610
+ while (i < s.length && s[i] !== '\n')
611
+ i++;
612
+ p.i = i;
613
+ continue;
614
+ }
615
+ p.i = i;
616
+ const a1 = parseSedAddr(p, isEre);
617
+ i = p.i;
618
+ while (i < s.length && s[i] === ' ')
619
+ i++;
620
+ let a2;
621
+ if (s[i] === ',') {
622
+ p.i = i + 1;
623
+ a2 = parseSedAddr(p, isEre);
624
+ if (a2 === undefined) {
625
+ throw new FauxnixParseError('fauxnix: sed expected address after ,');
626
+ }
627
+ i = p.i;
628
+ while (i < s.length && s[i] === ' ')
629
+ i++;
630
+ }
631
+ p.i = i;
632
+ if (p.i >= s.length)
633
+ throw new FauxnixParseError('fauxnix: sed missing command');
634
+ if (s[p.i] === '!') {
635
+ throw new FauxnixParseError('fauxnix: sed address negation is not supported yet');
636
+ }
637
+ const c = s[p.i];
638
+ if (c === 's') {
639
+ const d = s[p.i + 1];
640
+ if (d === undefined || /[a-zA-Z0-9\\]/.test(d)) {
641
+ throw new FauxnixParseError('fauxnix: sed invalid s command delimiter');
642
+ }
643
+ const readDelim = () => {
644
+ let j = p.i;
645
+ let body = '';
646
+ while (j < s.length && s[j] !== d) {
647
+ if (s[j] === '\\' && j + 1 < s.length) {
648
+ if (s[j + 1] === d)
649
+ body += d;
650
+ else
651
+ body += s[j] + s[j + 1];
652
+ j += 2;
653
+ continue;
654
+ }
655
+ body += s[j];
656
+ j++;
657
+ }
658
+ if (j >= s.length)
659
+ throw new FauxnixParseError('fauxnix: sed unterminated s command');
660
+ p.i = j + 1;
661
+ return body;
662
+ };
663
+ p.i = p.i + 2; // past 's' + delimiter
664
+ const re = readDelim();
665
+ const raw = readDelim();
666
+ let g = false;
667
+ let ci = false;
668
+ let pr = false;
669
+ let nth = 1;
670
+ while (p.i < s.length && /[0-9gipImM]/.test(s[p.i])) {
671
+ const f = s[p.i];
672
+ if (f === 'g')
673
+ g = true;
674
+ else if (f === 'i' || f === 'I')
675
+ ci = true;
676
+ else if (f === 'p')
677
+ pr = true;
678
+ else if (/[0-9]/.test(f)) {
679
+ const dm = s.slice(p.i).match(/^(\d+)/);
680
+ nth = parseInt(dm[1], 10);
681
+ if (nth === 0) {
682
+ throw new FauxnixParseError('fauxnix: sed s command number flag must be > 0');
683
+ }
684
+ p.i += dm[1].length - 1;
685
+ }
686
+ p.i++;
687
+ }
688
+ out.push({
689
+ k: 's',
690
+ a1,
691
+ a2,
692
+ re: isEre ? ereToDotNet(re) : breToDotNet(re),
693
+ repl: sedReplToNet(raw, d),
694
+ g,
695
+ ci,
696
+ p: pr,
697
+ nth,
698
+ });
699
+ continue;
700
+ }
701
+ if (c === 'y') {
702
+ const d = s[p.i + 1];
703
+ if (d === undefined || /[a-zA-Z0-9\\]/.test(d)) {
704
+ throw new FauxnixParseError('fauxnix: sed invalid y command delimiter');
705
+ }
706
+ p.i = p.i + 2; // past 'y' + delimiter
707
+ const readSet = () => {
708
+ const set = [];
709
+ while (p.i < s.length && s[p.i] !== d) {
710
+ if (s[p.i] === '\\' && p.i + 1 < s.length) {
711
+ const n = s[p.i + 1];
712
+ if (n === d)
713
+ set.push(d);
714
+ else if (n === 'n')
715
+ set.push('\n');
716
+ else if (n === 't')
717
+ set.push('\t');
718
+ else if (n === '\\')
719
+ set.push('\\');
720
+ else
721
+ set.push(n);
722
+ p.i += 2;
723
+ continue;
724
+ }
725
+ set.push(s[p.i]);
726
+ p.i++;
727
+ }
728
+ if (p.i >= s.length) {
729
+ throw new FauxnixParseError('fauxnix: sed unterminated y command');
730
+ }
731
+ p.i++; // past delimiter
732
+ return set;
733
+ };
734
+ const set1 = readSet();
735
+ const set2 = readSet();
736
+ if (set1.length !== set2.length) {
737
+ throw new FauxnixParseError("fauxnix: sed strings for 'y' command are different lengths");
738
+ }
739
+ out.push({ k: 'y', a1, a2, set1, set2 });
740
+ continue;
741
+ }
742
+ if (c === 'd' || c === 'p' || c === 'q') {
743
+ let j = p.i + 1;
744
+ let qn;
745
+ if (c === 'q') {
746
+ while (j < s.length && s[j] === ' ')
747
+ j++;
748
+ const dm = s.slice(j).match(/^(\d+)/);
749
+ if (dm) {
750
+ qn = parseInt(dm[1], 10);
751
+ j += dm[1].length;
752
+ }
753
+ }
754
+ p.i = j;
755
+ out.push({ k: c, a1, a2, qn });
756
+ continue;
757
+ }
758
+ const name = SED_UNSUPPORTED[c];
759
+ if (name)
760
+ throw new FauxnixParseError('fauxnix: ' + name + ' is not supported yet');
761
+ throw new FauxnixParseError("fauxnix: sed command '" + c + "' is not supported yet");
762
+ }
763
+ return out;
764
+ }
765
+ const sed = (args) => {
766
+ // custom argv parse: -i takes an ATTACHED suffix; -e/-f take attached or next
767
+ const raw = args.map((w) => wordToString(w));
768
+ let noPrint = false;
769
+ let isEre = false;
770
+ let suffix = null; // null = not in-place, '' = in-place no backup
771
+ const scripts = [];
772
+ const operandWords = [];
773
+ let i = 0;
774
+ let onlyOps = false;
775
+ while (i < raw.length) {
776
+ const a = raw[i];
777
+ if (!onlyOps && a === '--') {
778
+ onlyOps = true;
779
+ }
780
+ else if (!onlyOps && a.startsWith('--')) {
781
+ if (a === '--in-place' || a.startsWith('--in-place=')) {
782
+ suffix = a === '--in-place' ? '' : a.slice('--in-place='.length);
783
+ }
784
+ else if (a === '--regexp-extended')
785
+ isEre = true;
786
+ else if (a === '--quiet' || a === '--silent')
787
+ noPrint = true;
788
+ }
789
+ else if (!onlyOps && a.startsWith('-') && a.length > 1) {
790
+ const body = a.slice(1);
791
+ for (let c = 0; c < body.length; c++) {
792
+ const ch = body[c];
793
+ if (ch === 'n')
794
+ noPrint = true;
795
+ else if (ch === 'E' || ch === 'r')
796
+ isEre = true;
797
+ else if (ch === 's' || ch === 'u' || ch === 'z') {
798
+ /* accepted, no-op for us */
799
+ }
800
+ else if (ch === 'e' || ch === 'f') {
801
+ const rest = body.slice(c + 1);
802
+ let val;
803
+ if (rest) {
804
+ val = rest;
805
+ }
806
+ else if (i + 1 < raw.length) {
807
+ val = raw[i + 1];
808
+ i++;
809
+ }
810
+ else {
811
+ throw new FauxnixParseError('fauxnix: sed -' + ch + ' requires an argument');
812
+ }
813
+ if (ch === 'f') {
814
+ try {
815
+ val = readFileSync(nodePathOf(val), 'utf8');
816
+ }
817
+ catch {
818
+ throw new FauxnixParseError("fauxnix: sed can't read script file " + val);
819
+ }
820
+ }
821
+ scripts.push(val);
822
+ break;
823
+ }
824
+ else if (ch === 'i') {
825
+ suffix = body.slice(c + 1);
826
+ break;
827
+ }
828
+ else {
829
+ throw new FauxnixParseError('fauxnix: sed -' + ch + ' is not supported yet');
830
+ }
831
+ }
832
+ }
833
+ else {
834
+ operandWords.push(args[i]);
835
+ }
836
+ i++;
837
+ }
838
+ if (scripts.length === 0 && operandWords.length === 0) {
839
+ return "[Console]::Error.WriteLine('sed: no script was given'); $script:fx_exit = 1";
840
+ }
841
+ let scriptSrc;
842
+ if (scripts.length === 0) {
843
+ scriptSrc = wordToString(operandWords[0]);
844
+ operandWords.shift();
845
+ }
846
+ else {
847
+ scriptSrc = scripts.join('\n');
848
+ }
849
+ const cmds = parseSedScript(scriptSrc, isEre);
850
+ const inPlace = suffix !== null;
851
+ if (inPlace && operandWords.length === 0) {
852
+ return "[Console]::Error.WriteLine('sed: -i may not be used with stdin'); $script:fx_exit = 1";
853
+ }
854
+ const lines = [PS_READTEXT_FN, PS_SPLITLINES_FN];
855
+ if (operandWords.length > 0)
856
+ lines.push(PS_GLOB_FN);
857
+ // hoisted regex objects / y-arrays / range flags
858
+ const hoisted = [];
859
+ const rangeFlags = [];
860
+ cmds.forEach((cmd, idx) => {
861
+ if (cmd.k === 's') {
862
+ hoisted.push(cmd.ci
863
+ ? '$fx_r' + idx + ' = New-Object System.Text.RegularExpressions.Regex(' + psStr(cmd.re) + ', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)'
864
+ : '$fx_r' + idx + ' = New-Object System.Text.RegularExpressions.Regex(' + psStr(cmd.re) + ')');
865
+ }
866
+ else if (cmd.k === 'y') {
867
+ hoisted.push('$fx_y' + idx + 'a = [char[]](' + cmd.set1.map((ch) => ch.charCodeAt(0)).join(', ') + ')');
868
+ hoisted.push('$fx_y' + idx + 'b = [char[]](' + cmd.set2.map((ch) => ch.charCodeAt(0)).join(', ') + ')');
869
+ }
870
+ if (cmd.a1 && cmd.a2)
871
+ rangeFlags.push('$fx_rg' + idx + ' = $false');
872
+ });
873
+ // regex objects for regex addresses (deduped)
874
+ const addrVar = new Map();
875
+ for (const cmd of cmds) {
876
+ for (const a of [cmd.a1, cmd.a2]) {
877
+ if (a && a.k === 're') {
878
+ const key = a.re + '``' + String(a.ci);
879
+ if (!addrVar.has(key)) {
880
+ const name = '$fx_ar' + addrVar.size;
881
+ addrVar.set(key, name);
882
+ hoisted.push(a.ci
883
+ ? name + ' = New-Object System.Text.RegularExpressions.Regex(' + psStr(a.re) + ', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)'
884
+ : name + ' = New-Object System.Text.RegularExpressions.Regex(' + psStr(a.re) + ')');
885
+ }
886
+ }
887
+ }
888
+ }
889
+ const addrTest = (a) => {
890
+ switch (a.k) {
891
+ case 'line':
892
+ return '($fx_lno -eq ' + a.n + ')';
893
+ case 'last':
894
+ return '($fx_lno -eq $fx_n)';
895
+ case 're':
896
+ return '(' + addrVar.get(a.re + '``' + String(a.ci)) + '.IsMatch($fx_ps))';
897
+ case 'step': {
898
+ const first = a.first === 0 ? a.step : a.first;
899
+ return ('($fx_lno -ge ' + first + ' -and (($fx_lno - ' + first + ') % ' + a.step + ') -eq 0)');
900
+ }
901
+ }
902
+ };
903
+ const cmdBlocks = [];
904
+ cmds.forEach((cmd, idx) => {
905
+ const block = [];
906
+ if (cmd.a2 && cmd.a1) {
907
+ const bothNum = cmd.a1.k === 'line' && cmd.a2.k === 'line' && cmd.a2.n <= cmd.a1.n;
908
+ if (bothNum) {
909
+ block.push('$fx_sel = ' + addrTest(cmd.a1));
910
+ }
911
+ else {
912
+ block.push('if ($fx_rg' + idx + ') {');
913
+ block.push(' $fx_sel = $true');
914
+ block.push(' if (' + addrTest(cmd.a2) + ') { $fx_rg' + idx + ' = $false }');
915
+ block.push('} elseif (' + addrTest(cmd.a1) + ') {');
916
+ block.push(' $fx_sel = $true');
917
+ block.push(' $fx_rg' + idx + ' = $true');
918
+ block.push('} else {');
919
+ block.push(' $fx_sel = $false');
920
+ block.push('}');
921
+ }
922
+ }
923
+ else if (cmd.a1) {
924
+ block.push('$fx_sel = ' + addrTest(cmd.a1));
925
+ }
926
+ else {
927
+ block.push('$fx_sel = $true');
928
+ }
929
+ block.push('if ($fx_sel) {');
930
+ if (cmd.k === 's') {
931
+ block.push(' $fx_ms = $fx_r' + idx + '.Matches($fx_ps)');
932
+ block.push(' if ($fx_ms.Count -ge 1) {');
933
+ block.push(' $fx_sb = New-Object System.Text.StringBuilder');
934
+ block.push(' $fx_lastp = 0');
935
+ block.push(' $fx_kk = 0');
936
+ block.push(' $fx_did = $false');
937
+ block.push(' foreach ($fx_m in $fx_ms) {');
938
+ block.push(' $fx_kk++');
939
+ if (cmd.nth > 1)
940
+ block.push(' if ($fx_kk -lt ' + cmd.nth + ') { continue }');
941
+ block.push(' [void]$fx_sb.Append($fx_ps.Substring($fx_lastp, $fx_m.Index - $fx_lastp))');
942
+ block.push(' [void]$fx_sb.Append($fx_m.Result(' + psStrFlat(cmd.repl) + '))');
943
+ block.push(' $fx_lastp = $fx_m.Index + $fx_m.Length');
944
+ block.push(' $fx_did = $true');
945
+ if (!cmd.g)
946
+ block.push(' break');
947
+ block.push(' }');
948
+ block.push(' [void]$fx_sb.Append($fx_ps.Substring($fx_lastp))');
949
+ block.push(' $fx_ps = $fx_sb.ToString()');
950
+ if (cmd.p)
951
+ block.push(' if ($fx_did) { $fx_out.Add($fx_ps) }');
952
+ block.push(' }');
953
+ }
954
+ else if (cmd.k === 'y') {
955
+ block.push(' $fx_sb = New-Object System.Text.StringBuilder');
956
+ block.push(' foreach ($fx_ch in $fx_ps.ToCharArray()) {');
957
+ block.push(' $fx_ix = [array]::IndexOf($fx_y' + idx + 'a, $fx_ch)');
958
+ block.push(' if ($fx_ix -ge 0) { [void]$fx_sb.Append($fx_y' + idx + 'b[$fx_ix]) } else { [void]$fx_sb.Append($fx_ch) }');
959
+ block.push(' }');
960
+ block.push(' $fx_ps = $fx_sb.ToString()');
961
+ }
962
+ else if (cmd.k === 'd') {
963
+ block.push(' $fx_del = $true');
964
+ }
965
+ else if (cmd.k === 'p') {
966
+ block.push(' $fx_out.Add($fx_ps)');
967
+ }
968
+ else {
969
+ block.push(' $fx_stop = $true');
970
+ if (cmd.qn !== undefined)
971
+ block.push(' $fx_qn = ' + cmd.qn);
972
+ }
973
+ block.push('}');
974
+ cmdBlocks.push(block.join('\n'));
975
+ });
976
+ const loopBody = [];
977
+ loopBody.push('$fx_lno = $fx_i + 1');
978
+ loopBody.push('$fx_ps = $fx_lines[$fx_i]');
979
+ loopBody.push('$fx_del = $false');
980
+ cmdBlocks.forEach((blk, idx) => {
981
+ const guarded = blk
982
+ .split('\n')
983
+ .map((l) => (l === '' ? l : ' ' + l))
984
+ .join('\n');
985
+ if (idx === 0) {
986
+ loopBody.push(blk);
987
+ }
988
+ else {
989
+ loopBody.push('if (-not $fx_del -and -not $fx_stop) {');
990
+ loopBody.push(guarded);
991
+ loopBody.push('}');
992
+ }
993
+ });
994
+ loopBody.push('if (-not ' + pb(noPrint) + ' -and -not $fx_del) { $fx_out.Add($fx_ps) }');
995
+ const scanFile = [];
996
+ scanFile.push('$fx_lines = @(fx-splitlines (fx-read $fx_f))');
997
+ scanFile.push('$fx_n = $fx_lines.Count');
998
+ scanFile.push('$fx_out = New-Object System.Collections.Generic.List[string]');
999
+ scanFile.push('$fx_stop = $false');
1000
+ for (const f of rangeFlags)
1001
+ scanFile.push(f);
1002
+ scanFile.push('for ($fx_i = 0; $fx_i -lt $fx_n -and -not $fx_stop; $fx_i++) {');
1003
+ for (const l of loopBody)
1004
+ scanFile.push(' ' + l);
1005
+ scanFile.push('}');
1006
+ if (inPlace) {
1007
+ scanFile.push("if ($fx_out.Count -gt 0) { $fx_text = ($fx_out -join [string][char]10) + [string][char]10 } else { $fx_text = '' }");
1008
+ if (suffix !== '') {
1009
+ const sfx = suffix;
1010
+ scanFile.push('try { Copy-Item -LiteralPath $fx_f -Destination ($fx_f + ' + psStr(sfx) + ') -Force } catch {}');
1011
+ }
1012
+ scanFile.push('[IO.File]::WriteAllText($fx_f, $fx_text, (New-Object System.Text.UTF8Encoding($false)))');
1013
+ }
1014
+ else {
1015
+ scanFile.push('foreach ($fx_l in $fx_out) { $fx_l }');
1016
+ }
1017
+ lines.push(...hoisted);
1018
+ lines.push('$fx_qn = 0');
1019
+ if (operandWords.length > 0) {
1020
+ lines.push(...psCollectSources(psArray(operandWords), (v) => "[Console]::Error.WriteLine('sed: can''t read ' + " + v + " + ': No such file or directory')", true));
1021
+ lines.push('foreach ($fx_f in $fx_srcs) {');
1022
+ for (const l of scanFile)
1023
+ lines.push(' ' + l);
1024
+ lines.push('}');
1025
+ }
1026
+ else {
1027
+ lines.push('$fx_err = $false');
1028
+ lines.push('$fx_lines = ' + STDIN_LINES);
1029
+ lines.push('$fx_n = $fx_lines.Count');
1030
+ lines.push('$fx_out = New-Object System.Collections.Generic.List[string]');
1031
+ lines.push('$fx_stop = $false');
1032
+ for (const f of rangeFlags)
1033
+ lines.push(f);
1034
+ lines.push('for ($fx_i = 0; $fx_i -lt $fx_n -and -not $fx_stop; $fx_i++) {');
1035
+ for (const l of loopBody)
1036
+ lines.push(' ' + l);
1037
+ lines.push('}');
1038
+ lines.push('foreach ($fx_l in $fx_out) { $fx_l }');
1039
+ }
1040
+ lines.push('$script:fx_exit = $fx_qn');
1041
+ lines.push('if ($fx_err) { $script:fx_exit = 2 }');
1042
+ return lines.join('\n');
1043
+ };
1044
+ const AWK_UNSUPPORTED_FNS = [
1045
+ 'sin', 'cos', 'atan2', 'sqrt', 'int', 'exp', 'log', 'rand', 'srand',
1046
+ 'sprintf', 'system', 'index', 'split', 'sub', 'gsub', 'match',
1047
+ 'and', 'or', 'xor', 'compl', 'lshift', 'rshift', 'fflush', 'getline',
1048
+ ];
1049
+ class AwkParser {
1050
+ s;
1051
+ i;
1052
+ vars;
1053
+ regexes;
1054
+ constructor(s) {
1055
+ this.s = s;
1056
+ this.i = 0;
1057
+ this.vars = new Set();
1058
+ this.regexes = new Set();
1059
+ }
1060
+ ws() {
1061
+ while (this.i < this.s.length && ' \t\r\n'.includes(this.s[this.i]))
1062
+ this.i++;
1063
+ }
1064
+ wsSemi() {
1065
+ while (this.i < this.s.length && ' \t\r\n;'.includes(this.s[this.i]))
1066
+ this.i++;
1067
+ }
1068
+ atWord(w) {
1069
+ if (!this.s.startsWith(w, this.i))
1070
+ return false;
1071
+ const after = this.i + w.length;
1072
+ if (after < this.s.length && /[A-Za-z0-9_]/.test(this.s[after]))
1073
+ return false;
1074
+ return true;
1075
+ }
1076
+ parse() {
1077
+ const begin = [];
1078
+ const end = [];
1079
+ const items = [];
1080
+ this.wsSemi();
1081
+ while (this.i < this.s.length) {
1082
+ if (this.atWord('BEGIN')) {
1083
+ this.i += 5;
1084
+ begin.push(...this.parseAction());
1085
+ }
1086
+ else if (this.atWord('END')) {
1087
+ this.i += 3;
1088
+ end.push(...this.parseAction());
1089
+ }
1090
+ else if (this.s[this.i] === '{') {
1091
+ items.push({ pat: null, act: this.parseAction() });
1092
+ }
1093
+ else {
1094
+ const pat = this.parseExpr();
1095
+ this.ws();
1096
+ if (this.s[this.i] === ',') {
1097
+ throw new FauxnixParseError('fauxnix: awk range patterns are not supported yet');
1098
+ }
1099
+ if (this.s[this.i] === '{') {
1100
+ items.push({ pat, act: this.parseAction() });
1101
+ }
1102
+ else {
1103
+ items.push({ pat, act: null });
1104
+ }
1105
+ }
1106
+ this.wsSemi();
1107
+ }
1108
+ return { begin, items, end, vars: this.vars, regexes: [...this.regexes] };
1109
+ }
1110
+ parseAction() {
1111
+ this.ws();
1112
+ if (this.s[this.i] !== '{') {
1113
+ throw new FauxnixParseError('fauxnix: awk expected { to start an action');
1114
+ }
1115
+ this.i++;
1116
+ const stmts = [];
1117
+ this.wsSemi();
1118
+ while (this.i < this.s.length && this.s[this.i] !== '}') {
1119
+ stmts.push(this.parseStmt());
1120
+ this.wsSemi();
1121
+ }
1122
+ if (this.s[this.i] !== '}') {
1123
+ throw new FauxnixParseError('fauxnix: awk unterminated action block');
1124
+ }
1125
+ this.i++;
1126
+ return stmts;
1127
+ }
1128
+ parseStmt() {
1129
+ this.ws();
1130
+ const c = this.s[this.i];
1131
+ if (c === undefined)
1132
+ throw new FauxnixParseError('fauxnix: awk unexpected end of program');
1133
+ if (c === '{') {
1134
+ throw new FauxnixParseError('fauxnix: awk nested blocks are not supported yet');
1135
+ }
1136
+ if (this.atWord('print')) {
1137
+ this.i += 5;
1138
+ this.ws();
1139
+ const args = [];
1140
+ if (this.i < this.s.length &&
1141
+ this.s[this.i] !== '}' &&
1142
+ this.s[this.i] !== ';' &&
1143
+ this.s[this.i] !== '\n' &&
1144
+ this.s[this.i] !== '>') {
1145
+ args.push(this.parseExpr());
1146
+ this.ws();
1147
+ while (this.s[this.i] === ',') {
1148
+ this.i++;
1149
+ args.push(this.parseExpr());
1150
+ this.ws();
1151
+ }
1152
+ }
1153
+ if (this.s[this.i] === '>' || this.s[this.i] === '|') {
1154
+ throw new FauxnixParseError('fauxnix: awk print redirection is not supported yet');
1155
+ }
1156
+ return { k: 'print', args };
1157
+ }
1158
+ if (this.atWord('printf')) {
1159
+ this.i += 6;
1160
+ const fmt = this.parseExpr();
1161
+ if (fmt.k !== 'str') {
1162
+ throw new FauxnixParseError('fauxnix: awk printf format must be a string literal');
1163
+ }
1164
+ this.ws();
1165
+ const args = [];
1166
+ while (this.s[this.i] === ',') {
1167
+ this.i++;
1168
+ args.push(this.parseExpr());
1169
+ this.ws();
1170
+ }
1171
+ if (this.s[this.i] === '>' || this.s[this.i] === '|') {
1172
+ throw new FauxnixParseError('fauxnix: awk printf redirection is not supported yet');
1173
+ }
1174
+ return { k: 'printf', fmt: fmt.v, args };
1175
+ }
1176
+ if (this.atWord('exit')) {
1177
+ this.i += 4;
1178
+ this.ws();
1179
+ let code = null;
1180
+ if (this.i < this.s.length && !'};'.includes(this.s[this.i])) {
1181
+ code = this.parseExpr();
1182
+ }
1183
+ return { k: 'exit', code };
1184
+ }
1185
+ for (const kw of [
1186
+ 'if', 'for', 'while', 'do', 'next', 'getline', 'delete',
1187
+ 'break', 'continue', 'function', 'return',
1188
+ ]) {
1189
+ if (this.atWord(kw)) {
1190
+ throw new FauxnixParseError('fauxnix: awk ' + kw + ' is not supported yet');
1191
+ }
1192
+ }
1193
+ if (/[A-Za-z_]/.test(c)) {
1194
+ const m = this.s.slice(this.i).match(/^[A-Za-z_][A-Za-z0-9_]*/);
1195
+ const name = m[0];
1196
+ this.i += m[0].length;
1197
+ this.ws();
1198
+ const rest = this.s.slice(this.i);
1199
+ if (rest.startsWith('[')) {
1200
+ throw new FauxnixParseError('fauxnix: awk arrays are not supported yet');
1201
+ }
1202
+ if (rest.startsWith('(')) {
1203
+ throw new FauxnixParseError('fauxnix: awk user functions are not supported yet');
1204
+ }
1205
+ const opMatch = rest.match(/^(==|\+=|-=|\*=|\/=|%=|=)/);
1206
+ if (!opMatch || opMatch[1] === '==') {
1207
+ throw new FauxnixParseError('fauxnix: awk bare expression statements are not supported yet');
1208
+ }
1209
+ const op = opMatch[1];
1210
+ this.i += op.length;
1211
+ const e = this.parseExpr();
1212
+ this.vars.add(name);
1213
+ return { k: 'assign', name, op, e };
1214
+ }
1215
+ throw new FauxnixParseError("fauxnix: awk unexpected character '" + c + "' in program");
1216
+ }
1217
+ parseExpr() {
1218
+ return this.parseOr();
1219
+ }
1220
+ parseOr() {
1221
+ let l = this.parseAnd();
1222
+ this.ws();
1223
+ while (this.s.startsWith('||', this.i)) {
1224
+ this.i += 2;
1225
+ const r = this.parseAnd();
1226
+ l = { k: 'bin', op: '||', l, r };
1227
+ this.ws();
1228
+ }
1229
+ if (this.s[this.i] === '?') {
1230
+ throw new FauxnixParseError('fauxnix: awk ?: is not supported yet');
1231
+ }
1232
+ return l;
1233
+ }
1234
+ parseAnd() {
1235
+ let l = this.parseCmp();
1236
+ this.ws();
1237
+ while (this.s.startsWith('&&', this.i)) {
1238
+ this.i += 2;
1239
+ const r = this.parseCmp();
1240
+ l = { k: 'bin', op: '&&', l, r };
1241
+ this.ws();
1242
+ }
1243
+ return l;
1244
+ }
1245
+ parseCmp() {
1246
+ const l = this.parseConcat();
1247
+ this.ws();
1248
+ const two = this.s.slice(this.i, this.i + 2);
1249
+ if (['<=', '>=', '==', '!='].includes(two)) {
1250
+ this.i += 2;
1251
+ const r = this.parseConcat();
1252
+ return { k: 'bin', op: two, l, r };
1253
+ }
1254
+ const c = this.s[this.i];
1255
+ if (c === '<' || c === '>') {
1256
+ this.i++;
1257
+ const r = this.parseConcat();
1258
+ return { k: 'bin', op: c, l, r };
1259
+ }
1260
+ if (two === '!~' || c === '~') {
1261
+ const neg = two === '!~';
1262
+ this.i += neg ? 2 : 1;
1263
+ const rhs = this.parseConcat();
1264
+ if (rhs.k !== 'matchre' || rhs.lhs !== null) {
1265
+ throw new FauxnixParseError('fauxnix: awk dynamic regex is not supported yet');
1266
+ }
1267
+ return { k: 'matchre', re: rhs.re, neg, lhs: l };
1268
+ }
1269
+ return l;
1270
+ }
1271
+ parseConcat() {
1272
+ let l = this.parseAdd();
1273
+ for (;;) {
1274
+ this.ws();
1275
+ const c = this.s[this.i];
1276
+ if (c !== undefined &&
1277
+ (c === '"' || c === '(' || c === '$' || /[0-9]/.test(c) || /[A-Za-z_]/.test(c))) {
1278
+ const r = this.parseAdd();
1279
+ l = { k: 'bin', op: 'concat', l, r };
1280
+ }
1281
+ else {
1282
+ return l;
1283
+ }
1284
+ }
1285
+ }
1286
+ parseAdd() {
1287
+ let l = this.parseMul();
1288
+ for (;;) {
1289
+ this.ws();
1290
+ const c = this.s[this.i];
1291
+ if (c === '+' || c === '-') {
1292
+ this.i++;
1293
+ const r = this.parseMul();
1294
+ l = { k: 'bin', op: c, l, r };
1295
+ }
1296
+ else {
1297
+ return l;
1298
+ }
1299
+ }
1300
+ }
1301
+ parseMul() {
1302
+ let l = this.parseUnary();
1303
+ for (;;) {
1304
+ this.ws();
1305
+ const c = this.s[this.i];
1306
+ if (c === '*' || c === '/' || c === '%') {
1307
+ this.i++;
1308
+ const r = this.parseUnary();
1309
+ l = { k: 'bin', op: c, l, r };
1310
+ }
1311
+ else {
1312
+ return l;
1313
+ }
1314
+ }
1315
+ }
1316
+ parseUnary() {
1317
+ this.ws();
1318
+ const c = this.s[this.i];
1319
+ if (c === '!' && this.s[this.i + 1] !== '=') {
1320
+ this.i++;
1321
+ return { k: 'un', op: '!', e: this.parseUnary() };
1322
+ }
1323
+ if (c === '-') {
1324
+ this.i++;
1325
+ return { k: 'un', op: '-', e: this.parseUnary() };
1326
+ }
1327
+ if (c === '+') {
1328
+ this.i++;
1329
+ return { k: 'un', op: '+', e: this.parseUnary() };
1330
+ }
1331
+ return this.parsePrimary();
1332
+ }
1333
+ parsePrimary() {
1334
+ this.ws();
1335
+ const c = this.s[this.i];
1336
+ if (c === undefined) {
1337
+ throw new FauxnixParseError('fauxnix: awk unexpected end of expression');
1338
+ }
1339
+ if (c === '(') {
1340
+ this.i++;
1341
+ const e = this.parseExpr();
1342
+ this.ws();
1343
+ if (this.s[this.i] !== ')') {
1344
+ throw new FauxnixParseError('fauxnix: awk missing ) in expression');
1345
+ }
1346
+ this.i++;
1347
+ return e;
1348
+ }
1349
+ if (c === '$') {
1350
+ this.i++;
1351
+ const n = this.s[this.i];
1352
+ if (/[0-9]/.test(n)) {
1353
+ const m = this.s.slice(this.i).match(/^\d+/);
1354
+ this.i += m[0].length;
1355
+ return { k: 'field', idx: parseInt(m[0], 10) };
1356
+ }
1357
+ if (this.s.startsWith('NF', this.i)) {
1358
+ this.i += 2;
1359
+ return { k: 'fieldnf' };
1360
+ }
1361
+ throw new FauxnixParseError('fauxnix: awk $(...) fields are not supported yet');
1362
+ }
1363
+ if (c === '"') {
1364
+ let out = '';
1365
+ this.i++;
1366
+ while (this.i < this.s.length && this.s[this.i] !== '"') {
1367
+ if (this.s[this.i] === '\\' && this.i + 1 < this.s.length) {
1368
+ const n = this.s[this.i + 1];
1369
+ if (n === 'n')
1370
+ out += '\n';
1371
+ else if (n === 't')
1372
+ out += '\t';
1373
+ else if (n === '\\')
1374
+ out += '\\';
1375
+ else if (n === '"')
1376
+ out += '"';
1377
+ else
1378
+ out += n;
1379
+ this.i += 2;
1380
+ continue;
1381
+ }
1382
+ out += this.s[this.i];
1383
+ this.i++;
1384
+ }
1385
+ if (this.i >= this.s.length) {
1386
+ throw new FauxnixParseError('fauxnix: awk unterminated string literal');
1387
+ }
1388
+ this.i++;
1389
+ return { k: 'str', v: out };
1390
+ }
1391
+ if (c === '/') {
1392
+ let re = '';
1393
+ this.i++;
1394
+ while (this.i < this.s.length && this.s[this.i] !== '/') {
1395
+ if (this.s[this.i] === '\\' && this.i + 1 < this.s.length) {
1396
+ if (this.s[this.i + 1] === '/')
1397
+ re += '/';
1398
+ else
1399
+ re += this.s[this.i] + this.s[this.i + 1];
1400
+ this.i += 2;
1401
+ continue;
1402
+ }
1403
+ re += this.s[this.i];
1404
+ this.i++;
1405
+ }
1406
+ if (this.i >= this.s.length) {
1407
+ throw new FauxnixParseError('fauxnix: awk unterminated regex literal');
1408
+ }
1409
+ this.i++;
1410
+ this.regexes.add(re);
1411
+ return { k: 'matchre', re, neg: false, lhs: null };
1412
+ }
1413
+ if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(this.s[this.i + 1] ?? ''))) {
1414
+ const m = this.s.slice(this.i).match(/^(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?/);
1415
+ this.i += m[0].length;
1416
+ return { k: 'num', v: parseFloat(m[0]) };
1417
+ }
1418
+ if (/[A-Za-z_]/.test(c)) {
1419
+ const m = this.s.slice(this.i).match(/^[A-Za-z_][A-Za-z0-9_]*/);
1420
+ const name = m[0];
1421
+ this.i += m[0].length;
1422
+ if (name === 'NR')
1423
+ return { k: 'nr' };
1424
+ if (name === 'NF')
1425
+ return { k: 'nf' };
1426
+ if (name === 'length') {
1427
+ this.ws();
1428
+ if (this.s[this.i] === '(') {
1429
+ this.i++;
1430
+ this.ws();
1431
+ if (this.s[this.i] === ')') {
1432
+ this.i++;
1433
+ return { k: 'call', fn: 'length', args: [] };
1434
+ }
1435
+ const a = this.parseExpr();
1436
+ this.ws();
1437
+ if (this.s[this.i] !== ')') {
1438
+ throw new FauxnixParseError('fauxnix: awk missing ) after length(...)');
1439
+ }
1440
+ this.i++;
1441
+ return { k: 'call', fn: 'length', args: [a] };
1442
+ }
1443
+ return { k: 'call', fn: 'length', args: [] };
1444
+ }
1445
+ if (name === 'substr' || name === 'tolower' || name === 'toupper') {
1446
+ this.ws();
1447
+ if (this.s[this.i] !== '(') {
1448
+ throw new FauxnixParseError('fauxnix: awk ' + name + ' requires (...)');
1449
+ }
1450
+ this.i++;
1451
+ const args = [];
1452
+ this.ws();
1453
+ if (this.s[this.i] !== ')') {
1454
+ args.push(this.parseExpr());
1455
+ this.ws();
1456
+ while (this.s[this.i] === ',') {
1457
+ this.i++;
1458
+ args.push(this.parseExpr());
1459
+ this.ws();
1460
+ }
1461
+ }
1462
+ if (this.s[this.i] !== ')') {
1463
+ throw new FauxnixParseError('fauxnix: awk missing ) after ' + name + '(...)');
1464
+ }
1465
+ this.i++;
1466
+ const want = name === 'substr' ? 3 : 1;
1467
+ if (args.length > want) {
1468
+ throw new FauxnixParseError('fauxnix: awk too many arguments to ' + name);
1469
+ }
1470
+ return { k: 'call', fn: name, args };
1471
+ }
1472
+ if (AWK_UNSUPPORTED_FNS.includes(name)) {
1473
+ throw new FauxnixParseError('fauxnix: awk ' + name + ' is not supported yet');
1474
+ }
1475
+ this.vars.add(name);
1476
+ return { k: 'var', name };
1477
+ }
1478
+ throw new FauxnixParseError("fauxnix: awk unexpected character '" + c + "' in expression");
1479
+ }
1480
+ }
1481
+ function awkFmtToPs(fmt) {
1482
+ const chunks = [];
1483
+ const kinds = [];
1484
+ let lit = '';
1485
+ const flushLit = () => {
1486
+ if (lit !== '') {
1487
+ const flat = psStrFlat(lit.replace(/\{/g, '{{').replace(/\}/g, '}}'));
1488
+ chunks.push(flat === "''" ? flat : flat);
1489
+ lit = '';
1490
+ }
1491
+ };
1492
+ let i = 0;
1493
+ while (i < fmt.length) {
1494
+ const c = fmt[i];
1495
+ if (c === '%') {
1496
+ // %[-][width][.prec]conv — conv in s c d f
1497
+ const m = fmt.slice(i).match(/^%([-+0 ]*)(\d+)?(?:\.(\d+))?([sdfc])/);
1498
+ if (m) {
1499
+ const flags = m[1];
1500
+ const width = m[2];
1501
+ const prec = m[3];
1502
+ const conv = m[4];
1503
+ flushLit();
1504
+ const idx = kinds.length;
1505
+ let spec = '';
1506
+ if (conv === 'f') {
1507
+ spec = 'F' + (prec === undefined ? '6' : prec);
1508
+ }
1509
+ else if (conv === 'd' && prec !== undefined) {
1510
+ spec = 'D' + prec; // zero-pad, close to awk %.Nd
1511
+ }
1512
+ const align = width === undefined ? '' : ',' + (flags.includes('-') ? '-' : '') + width;
1513
+ chunks.push("'{" + idx + align + (spec === '' ? '' : ':' + spec) + "}'");
1514
+ kinds.push({ conv, align, spec });
1515
+ i += m[0].length;
1516
+ continue;
1517
+ }
1518
+ const n = fmt[i + 1];
1519
+ if (n === '%') {
1520
+ lit += '%';
1521
+ i += 2;
1522
+ continue;
1523
+ }
1524
+ if (n === undefined)
1525
+ break;
1526
+ throw new FauxnixParseError('fauxnix: awk printf %' + n + ' is not supported yet');
1527
+ }
1528
+ if (c === '\\' && i + 1 < fmt.length) {
1529
+ const n = fmt[i + 1];
1530
+ const code = n === 'n' ? 10 : n === 't' ? 9 : n === '\\' ? 92 : n === 'r' ? 13 : null;
1531
+ if (code === null) {
1532
+ lit += n;
1533
+ }
1534
+ else {
1535
+ flushLit();
1536
+ chunks.push('[string][char]' + code);
1537
+ }
1538
+ i += 2;
1539
+ continue;
1540
+ }
1541
+ if (c === '\n' || c === '\t' || c === '\r') {
1542
+ flushLit();
1543
+ chunks.push('[string][char]' + (c === '\n' ? 10 : c === '\t' ? 9 : 13));
1544
+ i++;
1545
+ continue;
1546
+ }
1547
+ lit += c;
1548
+ i++;
1549
+ }
1550
+ flushLit();
1551
+ const ps = chunks.length === 0 ? "''" : '(' + chunks.join(' + ') + ')';
1552
+ return { ps, kinds };
1553
+ }
1554
+ const awk = (args) => {
1555
+ const { values, operandWords } = parseWords(args, ['F', 'v']);
1556
+ // -v may repeat; collect all of them
1557
+ const vvars = [];
1558
+ for (let i = 0; i < args.length; i++) {
1559
+ const t = wordToString(args[i]);
1560
+ if (t === '-v' && i + 1 < args.length) {
1561
+ const nv = wordToString(args[i + 1]);
1562
+ const eq = nv.indexOf('=');
1563
+ if (eq > 0)
1564
+ vvars.push([nv.slice(0, eq), nv.slice(eq + 1)]);
1565
+ i++;
1566
+ }
1567
+ else if (t.startsWith('-v') && t.length > 2) {
1568
+ const nv = t.slice(2);
1569
+ const eq = nv.indexOf('=');
1570
+ if (eq > 0)
1571
+ vvars.push([nv.slice(0, eq), nv.slice(eq + 1)]);
1572
+ }
1573
+ }
1574
+ if (operandWords.length === 0) {
1575
+ return ("[Console]::Error.WriteLine('usage: awk [POSIX or GNU style options] -f progfile [--] file ...'); $script:fx_exit = 2");
1576
+ }
1577
+ const progWord = operandWords[0];
1578
+ const progLit = literalOfWord(progWord);
1579
+ if (progLit === null) {
1580
+ throw new FauxnixParseError('fauxnix: awk program must be a literal string');
1581
+ }
1582
+ const fileWords = operandWords.slice(1);
1583
+ const prog = new AwkParser(progLit).parse();
1584
+ // FS mode
1585
+ const fsRaw = values.get('-F') ?? ' ';
1586
+ let fsMode = 'ws';
1587
+ let fsChar = 32;
1588
+ let fsRe = '';
1589
+ {
1590
+ let fs = fsRaw;
1591
+ if (fs === '\\t')
1592
+ fs = '\t';
1593
+ else if (fs === '\\n')
1594
+ fs = '\n';
1595
+ if (fs === ' ')
1596
+ fsMode = 'ws';
1597
+ else if (fs.length === 1) {
1598
+ fsMode = 'char';
1599
+ fsChar = fs.charCodeAt(0);
1600
+ }
1601
+ else {
1602
+ fsMode = 'regex';
1603
+ fsRe = fs;
1604
+ }
1605
+ }
1606
+ const regexVar = new Map();
1607
+ const hoistedRegex = [];
1608
+ prog.regexes.forEach((re) => {
1609
+ const name = '$fx_ar' + regexVar.size;
1610
+ regexVar.set(re, name);
1611
+ hoistedRegex.push(name + ' = New-Object System.Text.RegularExpressions.Regex(' + psStr(ereToDotNet(re)) + ')');
1612
+ });
1613
+ const gen = (e) => {
1614
+ switch (e.k) {
1615
+ case 'num':
1616
+ return { ps: '([double]' + e.v + ')', bool: false };
1617
+ case 'str':
1618
+ return { ps: psStrFlat(e.v), bool: false };
1619
+ case 'var':
1620
+ return { ps: '$fxv_' + e.name, bool: false };
1621
+ case 'nr':
1622
+ return { ps: '$fx_nr', bool: false };
1623
+ case 'nf':
1624
+ return { ps: '$fx_nf', bool: false };
1625
+ case 'field':
1626
+ return e.idx === 0
1627
+ ? { ps: '$fx_line', bool: false }
1628
+ : { ps: '(fx-fld ' + e.idx + ')', bool: false };
1629
+ case 'fieldnf':
1630
+ return { ps: '(fx-fld $fx_nf)', bool: false };
1631
+ case 'matchre': {
1632
+ const v = regexVar.get(e.re);
1633
+ const target = e.lhs === null ? '$fx_line' : '(fx-str ' + gen(e.lhs).ps + ')';
1634
+ const test = v + '.IsMatch(' + target + ')';
1635
+ return { ps: e.neg ? '(-not (' + test + '))' : '(' + test + ')', bool: true };
1636
+ }
1637
+ case 'un': {
1638
+ const g = gen(e.e);
1639
+ if (e.op === '!') {
1640
+ return g.bool
1641
+ ? { ps: '(-not ' + g.ps + ')', bool: true }
1642
+ : { ps: '(-not (fx-true ' + g.ps + '))', bool: true };
1643
+ }
1644
+ if (e.op === '-')
1645
+ return { ps: '(-(fx-num ' + g.ps + '))', bool: false };
1646
+ return { ps: '(fx-num ' + g.ps + ')', bool: false };
1647
+ }
1648
+ case 'call': {
1649
+ if (e.fn === 'length') {
1650
+ const t = e.args.length === 0 ? '$fx_line' : '(fx-str ' + gen(e.args[0]).ps + ')';
1651
+ return { ps: '(' + t + ').Length', bool: false };
1652
+ }
1653
+ if (e.fn === 'tolower' || e.fn === 'toupper') {
1654
+ const t = '(fx-str ' + gen(e.args[0]).ps + ')';
1655
+ const m = e.fn === 'tolower' ? 'ToLower' : 'ToUpper';
1656
+ return { ps: '(' + t + ').' + m + '()', bool: false };
1657
+ }
1658
+ const s = '(fx-str ' + gen(e.args[0]).ps + ')';
1659
+ const a = e.args.length > 1 ? '(fx-num ' + gen(e.args[1]).ps + ')' : '([double]1)';
1660
+ const b = e.args.length > 2 ? '(fx-num ' + gen(e.args[2]).ps + ')' : '([double]9999999)';
1661
+ return { ps: '(fx-substr ' + s + ' ' + a + ' ' + b + ')', bool: false };
1662
+ }
1663
+ case 'bin': {
1664
+ if (e.op === 'concat') {
1665
+ return {
1666
+ ps: '((fx-str ' + gen(e.l).ps + ') + (fx-str ' + gen(e.r).ps + '))',
1667
+ bool: false,
1668
+ };
1669
+ }
1670
+ if (['<', '<=', '>', '>=', '==', '!='].includes(e.op)) {
1671
+ const map = {
1672
+ '<': 'lt',
1673
+ '<=': 'le',
1674
+ '>': 'gt',
1675
+ '>=': 'ge',
1676
+ '==': 'eq',
1677
+ '!=': 'ne',
1678
+ };
1679
+ return {
1680
+ ps: '(fx-cmp ' + gen(e.l).ps + ' ' + gen(e.r).ps + " '" + map[e.op] + "')",
1681
+ bool: true,
1682
+ };
1683
+ }
1684
+ if (e.op === '&&' || e.op === '||') {
1685
+ const gl = gen(e.l);
1686
+ const gr = gen(e.r);
1687
+ const lt = gl.bool ? gl.ps : '(fx-true ' + gl.ps + ')';
1688
+ const rt = gr.bool ? gr.ps : '(fx-true ' + gr.ps + ')';
1689
+ return {
1690
+ ps: '(' + lt + ' ' + (e.op === '&&' ? '-and' : '-or') + ' ' + rt + ')',
1691
+ bool: true,
1692
+ };
1693
+ }
1694
+ return {
1695
+ ps: '((fx-num ' + gen(e.l).ps + ') ' + e.op + ' (fx-num ' + gen(e.r).ps + '))',
1696
+ bool: false,
1697
+ };
1698
+ }
1699
+ }
1700
+ };
1701
+ const truth = (e) => {
1702
+ const g = gen(e);
1703
+ return g.bool ? g.ps : '(fx-true ' + g.ps + ')';
1704
+ };
1705
+ const genStmts = (stmts, inLoop) => {
1706
+ const out = [];
1707
+ for (const st of stmts) {
1708
+ if (st.k === 'print') {
1709
+ if (st.args.length === 0) {
1710
+ out.push('$fx_line');
1711
+ }
1712
+ else {
1713
+ out.push('(' + st.args.map((a) => '(fx-str ' + gen(a).ps + ')').join(" + ' ' + ") + ')');
1714
+ }
1715
+ }
1716
+ else if (st.k === 'printf') {
1717
+ const f = awkFmtToPs(st.fmt);
1718
+ const argExprs = f.kinds.map((kind, idx) => {
1719
+ const arg = st.args[idx];
1720
+ const raw = gen(arg ?? { k: 'num', v: 0 }).ps;
1721
+ if (kind.conv === 'd')
1722
+ return '([string][math]::Truncate((fx-num ' + raw + ')))';
1723
+ if (kind.conv === 'f')
1724
+ return '(fx-num ' + raw + ')';
1725
+ if (kind.conv === 'c') {
1726
+ // awk: numeric arg → char code, string arg → first char
1727
+ if (arg !== undefined && arg.k === 'num')
1728
+ return '[string][char]' + Math.trunc(arg.v);
1729
+ return '(fx-firstchar ' + raw + ')';
1730
+ }
1731
+ return '(fx-str ' + raw + ')';
1732
+ });
1733
+ const argList = argExprs.length ? ' ' + argExprs.join(', ') : '';
1734
+ out.push('(' + f.ps + ' -f' + argList + ')');
1735
+ }
1736
+ else if (st.k === 'assign') {
1737
+ const rhs = gen(st.e).ps;
1738
+ if (st.op === '=') {
1739
+ out.push('$fxv_' + st.name + ' = ' + rhs);
1740
+ }
1741
+ else {
1742
+ const arith = st.op.slice(0, 1);
1743
+ out.push('$fxv_' + st.name + ' = (fx-num $fxv_' + st.name + ') ' + arith + ' (fx-num ' + rhs + ')');
1744
+ }
1745
+ }
1746
+ else {
1747
+ const code = st.code ? '([int](fx-num ' + gen(st.code).ps + '))' : '0';
1748
+ out.push('$script:fx_exit = ' + code);
1749
+ out.push('$fx_exitq = $true');
1750
+ if (inLoop)
1751
+ out.push('break');
1752
+ }
1753
+ }
1754
+ return out;
1755
+ };
1756
+ const lines = [PS_READTEXT_FN, PS_SPLITLINES_FN];
1757
+ if (fileWords.length > 0)
1758
+ lines.push(PS_GLOB_FN);
1759
+ lines.push('function fx-num($v) {', ' if ($v -is [double]) { return $v }', ' if ($v -is [int] -or $v -is [long]) { return [double]$v }', ' if ($null -eq $v) { return [double]0 }', ' $s = [string]$v', " if ($s -match '^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$') { return [double]$s }", ' return [double]0', '}', 'function fx-fnum($v) {', ' if ($v -eq [math]::Floor($v) -and [math]::Abs($v) -lt 1e15) { return ([string][long]$v) }', " return ('{0:G6}' -f $v)", '}', 'function fx-str($v) {', ' if ($v -is [double]) { return (fx-fnum $v) }', " if ($null -eq $v) { return '' }", ' return [string]$v', '}', 'function fx-true($v) {', ' if ($v -is [double] -or $v -is [int]) { return ($v -ne 0) }', ' if ($null -eq $v) { return $false }', ' $s = [string]$v', " if ($s -eq '' -or $s -eq '0') { return $false }", ' return $true', '}', 'function fx-cmp($a, $b, $op) {', ' $ad = $null', ' $bd = $null', ' if ($a -is [double] -or $a -is [int]) { $ad = [double]$a }', " elseif ($a -match '^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$') { $ad = [double]$a }", ' if ($b -is [double] -or $b -is [int]) { $bd = [double]$b }', " elseif ($b -match '^[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?$') { $bd = [double]$b }", ' $r = 0', ' if ($null -ne $ad -and $null -ne $bd) {', ' if ($ad -lt $bd) { $r = -1 } elseif ($ad -gt $bd) { $r = 1 }', ' } else {', ' $r = [string]::CompareOrdinal((fx-str $a), (fx-str $b))', ' }', " if ($op -eq 'lt') { return ($r -lt 0) }", " if ($op -eq 'le') { return ($r -le 0) }", " if ($op -eq 'gt') { return ($r -gt 0) }", " if ($op -eq 'ge') { return ($r -ge 0) }", " if ($op -eq 'eq') { return ($r -eq 0) }", ' return ($r -ne 0)', '}', 'function fx-fld($n) {', ' if ($n -ge 1 -and $n -le $fx_flds.Count) { return [string]$fx_flds[$n - 1] }', " return ''", '}', 'function fx-substr($s, $a, $b) {', ' $t = [string]$s', ' $st = [int]$a - 1', ' if ($st -lt 0) { $b = $b + $st; $st = 0 }', " if ($b -lt 1 -or $st -ge $t.Length) { return '' }", ' $len = [math]::Min([int]$b, $t.Length - $st)', ' return $t.Substring($st, $len)', '}', 'function fx-firstchar($v) {', ' $t = [string]$v', ' if ($t.Length -ge 1) { return ([string]$t[0]) }', " return ''", '}');
1760
+ lines.push(...hoistedRegex);
1761
+ for (const v of prog.vars)
1762
+ lines.push('$fxv_' + v + ' = $null');
1763
+ for (const [n, v] of vvars) {
1764
+ lines.push('$fxv_' + n + ' = ' + (/^-?(\d+\.?\d*|\.\d+)$/.test(v) ? '[double]' + v : psStr(v)));
1765
+ }
1766
+ lines.push('$fx_exitq = $false');
1767
+ if (prog.begin.length > 0)
1768
+ lines.push(...genStmts(prog.begin, false));
1769
+ lines.push('$fx_nr = 0');
1770
+ if (fileWords.length > 0) {
1771
+ lines.push(...psCollectSources(psArray(fileWords), (v) => "[Console]::Error.WriteLine('awk: fatal: cannot open file ' + [string][char]96 + " +
1772
+ v +
1773
+ " + [string][char]39 + ' for reading (No such file or directory)')", true));
1774
+ lines.push('$fx_lines = @()');
1775
+ lines.push('foreach ($fx_f in $fx_srcs) { $fx_lines += fx-splitlines (fx-read $fx_f) }');
1776
+ }
1777
+ else {
1778
+ lines.push('$fx_err = $false');
1779
+ lines.push('$fx_lines = ' + STDIN_LINES);
1780
+ }
1781
+ const mainLoop = [];
1782
+ if (fsMode === 'ws') {
1783
+ mainLoop.push("if ($fx_line -eq '') { $fx_flds = @(); $fx_nf = 0 }");
1784
+ mainLoop.push('else {');
1785
+ mainLoop.push(" $fx_t = $fx_line.Trim(' ', [char]9)");
1786
+ mainLoop.push(" if ($fx_t -eq '') { $fx_flds = @(); $fx_nf = 0 }");
1787
+ mainLoop.push(" else { $fx_flds = @($fx_t -split '[ \\t]+'); $fx_nf = $fx_flds.Count }");
1788
+ mainLoop.push('}');
1789
+ }
1790
+ else if (fsMode === 'char') {
1791
+ mainLoop.push("if ($fx_line -eq '') { $fx_flds = @(); $fx_nf = 0 }");
1792
+ mainLoop.push('else { $fx_flds = @($fx_line.Split([char]' + fsChar + ')); $fx_nf = $fx_flds.Count }');
1793
+ }
1794
+ else {
1795
+ mainLoop.push("if ($fx_line -eq '') { $fx_flds = @(); $fx_nf = 0 }");
1796
+ mainLoop.push('else { $fx_flds = @($fx_line -split ' + psStr(fsRe) + '); $fx_nf = $fx_flds.Count }');
1797
+ }
1798
+ for (const item of prog.items) {
1799
+ const stmts = genStmts(item.act ?? [{ k: 'print', args: [] }], true);
1800
+ if (item.pat === null) {
1801
+ for (const st of stmts)
1802
+ mainLoop.push(st);
1803
+ }
1804
+ else {
1805
+ mainLoop.push('if (' + truth(item.pat) + ') {');
1806
+ for (const st of stmts)
1807
+ mainLoop.push(' ' + st);
1808
+ mainLoop.push('}');
1809
+ }
1810
+ }
1811
+ lines.push('if (-not $fx_exitq) {');
1812
+ lines.push('foreach ($fx_line in $fx_lines) {');
1813
+ lines.push(' $fx_nr++');
1814
+ for (const l of mainLoop)
1815
+ lines.push(' ' + l);
1816
+ lines.push('}');
1817
+ lines.push('}');
1818
+ if (prog.end.length > 0)
1819
+ lines.push(...genStmts(prog.end, false));
1820
+ lines.push('if ($fx_err) { $script:fx_exit = 2 }');
1821
+ return lines.join('\n');
1822
+ };
1823
+ function parseSortKeySpec(spec, g) {
1824
+ const parts = spec.split(',');
1825
+ if (parts.length > 2) {
1826
+ throw new FauxnixParseError('fauxnix: sort -k ' + spec + ' is not supported yet');
1827
+ }
1828
+ const parsePart = (p) => {
1829
+ const m = p.match(/^(\d+)(?:\.(\d+))?([nrbf]*)$/);
1830
+ if (!m)
1831
+ throw new FauxnixParseError('fauxnix: sort -k ' + spec + ' is not supported yet');
1832
+ if (m[2] !== undefined) {
1833
+ throw new FauxnixParseError('fauxnix: sort -k character positions are not supported yet');
1834
+ }
1835
+ return { f: parseInt(m[1], 10), mods: m[3] };
1836
+ };
1837
+ const a = parsePart(parts[0]);
1838
+ const bb = parts.length === 2 ? parsePart(parts[1]) : { f: 2147483647, mods: '' };
1839
+ if (parts.length === 2 && bb.f < a.f) {
1840
+ throw new FauxnixParseError('fauxnix: sort -k ' + spec + ' is not supported yet');
1841
+ }
1842
+ return {
1843
+ from: a.f,
1844
+ to: bb.f,
1845
+ n: a.mods.includes('n') || bb.mods.includes('n') || g.n,
1846
+ r: a.mods.includes('r') || bb.mods.includes('r'),
1847
+ b: a.mods.includes('b') || bb.mods.includes('b') || g.b,
1848
+ f: a.mods.includes('f') || bb.mods.includes('f') || g.f,
1849
+ };
1850
+ }
1851
+ const sort = (args) => {
1852
+ const { flags, longs, values, operandWords } = parseWords(args, ['t', 'k'], []);
1853
+ const globalR = flags.has('r') || longs.has('--reverse');
1854
+ const globalN = flags.has('n') || longs.has('--numeric-sort');
1855
+ const uniqMode = flags.has('u') || longs.has('--unique');
1856
+ const globalF = flags.has('f') || longs.has('--ignore-case');
1857
+ const globalB = flags.has('b') || longs.has('--ignore-leading-blanks');
1858
+ let sepChar = -1; // -1 = whitespace mode
1859
+ const t = values.get('-t');
1860
+ if (t !== undefined) {
1861
+ let tv = t;
1862
+ if (tv === '\\t')
1863
+ tv = '\t';
1864
+ if (tv.length !== 1) {
1865
+ throw new FauxnixParseError('fauxnix: sort multi-character tab is not supported yet');
1866
+ }
1867
+ sepChar = tv.charCodeAt(0);
1868
+ }
1869
+ const specs = [];
1870
+ for (const k of collectShortValues(args, 'k')) {
1871
+ specs.push(parseSortKeySpec(k, { n: globalN, b: globalB, f: globalF }));
1872
+ }
1873
+ const lines = [PS_READTEXT_FN, PS_SPLITLINES_FN, PS_GLOB_FN];
1874
+ lines.push(...psCollectSources(psArray(operandWords), (v) => "[Console]::Error.WriteLine('sort: cannot read: ' + " + v + " + ': No such file or directory')", false));
1875
+ if (operandWords.length > 0) {
1876
+ lines.push('$fx_lines = @()');
1877
+ lines.push('foreach ($fx_f in $fx_srcs) { $fx_lines += fx-splitlines (fx-read $fx_f) }');
1878
+ }
1879
+ else {
1880
+ lines.push('$fx_lines = ' + STDIN_LINES);
1881
+ }
1882
+ const fastPath = specs.length === 0 && !globalN && !globalB;
1883
+ if (fastPath) {
1884
+ const comparer = globalF ? 'OrdinalIgnoreCase' : 'Ordinal';
1885
+ lines.push('$fx_arr = [string[]]$fx_lines');
1886
+ lines.push('[array]::Sort($fx_arr, [System.StringComparer]::' + comparer + ')');
1887
+ if (globalR)
1888
+ lines.push('[array]::Reverse($fx_arr)');
1889
+ if (uniqMode) {
1890
+ lines.push('$fx_res = New-Object System.Collections.Generic.List[string]');
1891
+ lines.push('if ($fx_arr.Count -gt 0) { $fx_res.Add($fx_arr[0]) }');
1892
+ lines.push('for ($fx_i = 1; $fx_i -lt $fx_arr.Count; $fx_i++) {');
1893
+ lines.push(' if (-not [System.StringComparer]::' +
1894
+ comparer +
1895
+ '.Equals($fx_arr[$fx_i - 1], $fx_arr[$fx_i])) { $fx_res.Add($fx_arr[$fx_i]) }');
1896
+ lines.push('}');
1897
+ lines.push('$fx_arr = $fx_res.ToArray()');
1898
+ }
1899
+ lines.push('foreach ($fx_l in $fx_arr) { $fx_l }');
1900
+ }
1901
+ else {
1902
+ const sepSplit = sepChar >= 0
1903
+ ? '$fs = $line.Split([char]' + sepChar + ')'
1904
+ : "$t = $line.Trim(' ', [char]9); if ($t -eq '') { $fs = @() } else { $fs = @($t -split '[ \\t]+') }";
1905
+ const joiner = sepChar >= 0 ? '[string][char]' + sepChar : "' '";
1906
+ lines.push('function fx-keyof($line, $from, $to) {', ' ' + sepSplit, " if ($fs.Count -lt $from) { return '' }", ' $e2 = $to; if ($e2 -gt $fs.Count) { $e2 = $fs.Count }', ' return ($fs[($from - 1)..($e2 - 1)] -join ' + joiner + ')', '}', 'function fx-numkey($s) {', " if ($s -match '^[ \\t]*[-+]?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?') { return [double]($Matches[0].Trim()) }", ' return [double]0', '}');
1907
+ const cmpBody = (lastResort) => {
1908
+ const body = [];
1909
+ const allSpecs = specs.length > 0
1910
+ ? specs
1911
+ : [{ from: 1, to: 2147483647, n: globalN, r: false, b: globalB, f: globalF }];
1912
+ for (const spec of allSpecs) {
1913
+ const flip = spec.r !== globalR; // XOR
1914
+ let ka = '(fx-keyof $x ' + spec.from + ' ' + spec.to + ')';
1915
+ let kb = '(fx-keyof $y ' + spec.from + ' ' + spec.to + ')';
1916
+ if (spec.b) {
1917
+ const trimChars = sepChar >= 0 ? '[char]' + sepChar : "' ', [char]9";
1918
+ ka = '(' + ka + ').TrimStart(' + trimChars + ')';
1919
+ kb = '(' + kb + ').TrimStart(' + trimChars + ')';
1920
+ }
1921
+ if (spec.n) {
1922
+ ka = '(fx-numkey ' + ka + ')';
1923
+ kb = '(fx-numkey ' + kb + ')';
1924
+ body.push(' $ka = ' + ka);
1925
+ body.push(' $kb = ' + kb);
1926
+ if (flip) {
1927
+ body.push(' if ($ka -lt $kb) { return 1 }');
1928
+ body.push(' if ($ka -gt $kb) { return -1 }');
1929
+ }
1930
+ else {
1931
+ body.push(' if ($ka -lt $kb) { return -1 }');
1932
+ body.push(' if ($ka -gt $kb) { return 1 }');
1933
+ }
1934
+ }
1935
+ else {
1936
+ if (spec.f) {
1937
+ ka = '(' + ka + ').ToLower()';
1938
+ kb = '(' + kb + ').ToLower()';
1939
+ }
1940
+ body.push(' $ka = ' + ka);
1941
+ body.push(' $kb = ' + kb);
1942
+ body.push(' $r = [string]::CompareOrdinal($ka, $kb)');
1943
+ if (flip)
1944
+ body.push(' if ($r -ne 0) { return (-$r) }');
1945
+ else
1946
+ body.push(' if ($r -ne 0) { return $r }');
1947
+ }
1948
+ }
1949
+ body.push(lastResort
1950
+ ? globalR
1951
+ ? ' return (-[string]::CompareOrdinal($x, $y))'
1952
+ : ' return [string]::CompareOrdinal($x, $y)'
1953
+ : ' return 0');
1954
+ return body;
1955
+ };
1956
+ lines.push('function fx-cmp2($x, $y) {', ...cmpBody(true), '}');
1957
+ if (uniqMode) {
1958
+ lines.push('function fx-ucmp($x, $y) {', ...cmpBody(false), '}');
1959
+ lines.push('function fx-msortu($a) {', ' if ($a.Count -le 1) { return @($a) }', ' $mid = [int]($a.Count / 2)', ' $l = @(fx-msortu @($a[0..($mid - 1)]))', ' $r = @(fx-msortu @($a[$mid..($a.Count - 1)]))', ' $o = New-Object System.Collections.Generic.List[string]', ' $i = 0; $j = 0', ' while ($i -lt $l.Count -and $j -lt $r.Count) {', ' if ((fx-ucmp $l[$i] $r[$j]) -le 0) { [void]$o.Add($l[$i]); $i++ } else { [void]$o.Add($r[$j]); $j++ }', ' }', ' while ($i -lt $l.Count) { [void]$o.Add($l[$i]); $i++ }', ' while ($j -lt $r.Count) { [void]$o.Add($r[$j]); $j++ }', ' return $o.ToArray()', '}');
1960
+ }
1961
+ lines.push('function fx-msort($a) {', ' if ($a.Count -le 1) { return @($a) }', ' $mid = [int]($a.Count / 2)', ' $l = @(fx-msort @($a[0..($mid - 1)]))', ' $r = @(fx-msort @($a[$mid..($a.Count - 1)]))', ' $o = New-Object System.Collections.Generic.List[string]', ' $i = 0; $j = 0', ' while ($i -lt $l.Count -and $j -lt $r.Count) {', ' if ((fx-cmp2 $l[$i] $r[$j]) -le 0) { [void]$o.Add($l[$i]); $i++ } else { [void]$o.Add($r[$j]); $j++ }', ' }', ' while ($i -lt $l.Count) { [void]$o.Add($l[$i]); $i++ }', ' while ($j -lt $r.Count) { [void]$o.Add($r[$j]); $j++ }', ' return $o.ToArray()', '}', '$fx_arr = @(fx-msort @($fx_lines))');
1962
+ if (uniqMode) {
1963
+ // GNU: -u disables the last-resort comparison — sort by keys only, stable
1964
+ lines.push('$fx_arr2 = @()');
1965
+ lines.push('foreach ($fx_l in $fx_lines) { $fx_arr2 += [string]$fx_l }');
1966
+ lines.push('$fx_arr = @(fx-msortu @($fx_arr2))');
1967
+ lines.push('$fx_res = New-Object System.Collections.Generic.List[string]');
1968
+ lines.push('if ($fx_arr.Count -gt 0) { $fx_res.Add($fx_arr[0]) }');
1969
+ lines.push('for ($fx_i = 1; $fx_i -lt $fx_arr.Count; $fx_i++) {');
1970
+ lines.push(' if ((fx-ucmp $fx_arr[$fx_i - 1] $fx_arr[$fx_i]) -ne 0) { $fx_res.Add($fx_arr[$fx_i]) }');
1971
+ lines.push('}');
1972
+ lines.push('$fx_arr = $fx_res.ToArray()');
1973
+ }
1974
+ lines.push('foreach ($fx_l in $fx_arr) { $fx_l }');
1975
+ }
1976
+ lines.push('$script:fx_exit = 0');
1977
+ lines.push('if ($fx_err) { $script:fx_exit = 2 }');
1978
+ return lines.join('\n');
1979
+ };
1980
+ /* ------------------------------------------------------------------ */
1981
+ /* uniq */
1982
+ /* ------------------------------------------------------------------ */
1983
+ const uniq = (args) => {
1984
+ const { flags, operandWords } = parseWords(args);
1985
+ const count = flags.has('c');
1986
+ const dupOnly = flags.has('d');
1987
+ const uniqOnly = flags.has('u');
1988
+ const ignoreCase = flags.has('i');
1989
+ const inFile = operandWords.length > 0 ? operandWords[0] : null;
1990
+ const outFile = operandWords.length > 1 ? operandWords[1] : null;
1991
+ const lines = [PS_READTEXT_FN, PS_SPLITLINES_FN];
1992
+ if (inFile !== null) {
1993
+ lines.push(PS_GLOB_FN);
1994
+ lines.push(...psCollectSources('@(' + operandExpr(inFile) + ')', (v) => "[Console]::Error.WriteLine('uniq: ' + " + v + " + ': No such file or directory')", false));
1995
+ lines.push('if ($fx_srcs.Count -gt 0) { $fx_lines = @(fx-splitlines (fx-read $fx_srcs[0])) }');
1996
+ lines.push('else { $fx_lines = @() }');
1997
+ }
1998
+ else {
1999
+ lines.push('$fx_err = $false');
2000
+ lines.push('$fx_lines = ' + STDIN_LINES);
2001
+ }
2002
+ lines.push('function fx-ueq($a, $b) {');
2003
+ if (ignoreCase) {
2004
+ lines.push(' return (([string]$a).ToLower() -ceq ([string]$b).ToLower())');
2005
+ }
2006
+ else {
2007
+ lines.push(' return (([string]$a) -ceq ([string]$b))');
2008
+ }
2009
+ lines.push('}');
2010
+ lines.push('function fx-uemit($l, $c) {');
2011
+ if (dupOnly) {
2012
+ lines.push(' if ($c -gt 1) { $fx_res.Add($l) }');
2013
+ }
2014
+ else if (uniqOnly) {
2015
+ lines.push(' if ($c -eq 1) { $fx_res.Add($l) }');
2016
+ }
2017
+ else if (count) {
2018
+ lines.push(" $fx_res.Add(('{0,7} {1}' -f $c, $l))");
2019
+ }
2020
+ else {
2021
+ lines.push(' $fx_res.Add($l)');
2022
+ }
2023
+ lines.push('}');
2024
+ lines.push('$fx_res = New-Object System.Collections.Generic.List[string]');
2025
+ lines.push('$fx_prev = $null');
2026
+ lines.push('$fx_cnt = 0');
2027
+ lines.push('foreach ($fx_l in $fx_lines) {');
2028
+ lines.push(' if ($null -eq $fx_prev) { $fx_prev = [string]$fx_l; $fx_cnt = 1; continue }');
2029
+ lines.push(' if (fx-ueq $fx_prev $fx_l) { $fx_cnt++ }');
2030
+ lines.push(' else { fx-uemit $fx_prev $fx_cnt; $fx_prev = [string]$fx_l; $fx_cnt = 1 }');
2031
+ lines.push('}');
2032
+ lines.push('if ($null -ne $fx_prev) { fx-uemit $fx_prev $fx_cnt }');
2033
+ if (outFile !== null) {
2034
+ lines.push('[IO.File]::WriteAllText(' +
2035
+ operandExpr(outFile) +
2036
+ ", ($fx_res -join [string][char]10) + [string][char]10, (New-Object System.Text.UTF8Encoding($false)))");
2037
+ }
2038
+ else {
2039
+ lines.push('foreach ($fx_l in $fx_res) { $fx_l }');
2040
+ }
2041
+ lines.push('$script:fx_exit = 0');
2042
+ lines.push('if ($fx_err) { $script:fx_exit = 1 }');
2043
+ return lines.join('\n');
2044
+ };
2045
+ function parseCutList(list) {
2046
+ const ranges = [];
2047
+ for (const part of list.split(',')) {
2048
+ const m = part.match(/^(\d+)(-(\d*)?)?$/);
2049
+ if (!m)
2050
+ throw new FauxnixParseError('fauxnix: cut invalid list "' + list + '"');
2051
+ const from = parseInt(m[1], 10);
2052
+ if (m[2] === undefined)
2053
+ ranges.push({ from, to: from });
2054
+ else if (m[2] === '-')
2055
+ ranges.push({ from, to: 2147483647 });
2056
+ else {
2057
+ const to = parseInt(m[3], 10);
2058
+ if (to < from) {
2059
+ throw new FauxnixParseError('fauxnix: cut invalid range in list "' + list + '"');
2060
+ }
2061
+ ranges.push({ from, to });
2062
+ }
2063
+ }
2064
+ return ranges;
2065
+ }
2066
+ const cut = (args) => {
2067
+ const { flags, longs, values, operandWords } = parseWords(args, ['d', 'f', 'c', 'b'], []);
2068
+ const complement = longs.has('--complement');
2069
+ const charsMode = flags.has('c') || flags.has('b');
2070
+ const fieldsMode = flags.has('f');
2071
+ const suppress = flags.has('s');
2072
+ if (charsMode && fieldsMode) {
2073
+ throw new FauxnixParseError('fauxnix: cut only one type of list may be specified');
2074
+ }
2075
+ if (!charsMode && !fieldsMode) {
2076
+ throw new FauxnixParseError('fauxnix: cut you must specify a list of bytes, characters, or fields');
2077
+ }
2078
+ const list = values.get('-c') ?? values.get('-b') ?? values.get('-f');
2079
+ if (list === undefined)
2080
+ throw new FauxnixParseError('fauxnix: cut requires a list');
2081
+ const ranges = parseCutList(list);
2082
+ let delimCode = 9; // tab
2083
+ const d = values.get('-d');
2084
+ if (d !== undefined) {
2085
+ let dv = d;
2086
+ if (dv === '\\t')
2087
+ dv = '\t';
2088
+ else if (dv === '\\n')
2089
+ dv = '\n';
2090
+ if (dv.length !== 1) {
2091
+ throw new FauxnixParseError('fauxnix: cut the delimiter must be a single character');
2092
+ }
2093
+ delimCode = dv.charCodeAt(0);
2094
+ }
2095
+ const inTest = ranges
2096
+ .map((r) => '($p -ge ' + r.from + ' -and $p -le ' + r.to + ')')
2097
+ .join(' -or ');
2098
+ const testExpr = complement ? '(-not (' + inTest + '))' : '(' + inTest + ')';
2099
+ const lines = [PS_READTEXT_FN, PS_SPLITLINES_FN, PS_GLOB_FN];
2100
+ lines.push(...psCollectSources(psArray(operandWords), (v) => "[Console]::Error.WriteLine('cut: ' + " + v + " + ': No such file or directory')", false));
2101
+ if (operandWords.length > 0) {
2102
+ lines.push('$fx_lines = @()');
2103
+ lines.push('foreach ($fx_f in $fx_srcs) { $fx_lines += fx-splitlines (fx-read $fx_f) }');
2104
+ }
2105
+ else {
2106
+ lines.push('$fx_lines = ' + STDIN_LINES);
2107
+ }
2108
+ if (charsMode) {
2109
+ lines.push('foreach ($fx_l in $fx_lines) {');
2110
+ lines.push(' $fx_cs = $fx_l.ToCharArray()');
2111
+ lines.push(' $fx_sb = New-Object System.Text.StringBuilder');
2112
+ lines.push(' for ($fx_k = 0; $fx_k -lt $fx_cs.Count; $fx_k++) {');
2113
+ lines.push(' $p = $fx_k + 1');
2114
+ lines.push(' if ' + testExpr + ' { [void]$fx_sb.Append($fx_cs[$fx_k]) }');
2115
+ lines.push(' }');
2116
+ lines.push(' $fx_sb.ToString()');
2117
+ lines.push('}');
2118
+ }
2119
+ else {
2120
+ lines.push('foreach ($fx_l in $fx_lines) {');
2121
+ lines.push(' if (-not $fx_l.Contains([string][char]' + delimCode + ')) {');
2122
+ if (!suppress)
2123
+ lines.push(' $fx_l');
2124
+ lines.push(' } else {');
2125
+ lines.push(' $fx_fs = $fx_l.Split([char]' + delimCode + ')');
2126
+ lines.push(' $fx_sel = @()');
2127
+ lines.push(' for ($p = 1; $p -le $fx_fs.Count; $p++) {');
2128
+ lines.push(' if ' + testExpr + ' { $fx_sel += $fx_fs[$p - 1] }');
2129
+ lines.push(' }');
2130
+ lines.push(' ($fx_sel -join [string][char]' + delimCode + ')');
2131
+ lines.push(' }');
2132
+ lines.push('}');
2133
+ }
2134
+ lines.push('$script:fx_exit = 0');
2135
+ lines.push('if ($fx_err) { $script:fx_exit = 1 }');
2136
+ return lines.join('\n');
2137
+ };
2138
+ /* ------------------------------------------------------------------ */
2139
+ /* tr */
2140
+ /* ------------------------------------------------------------------ */
2141
+ function trClassCodes(name) {
2142
+ const seq = (a, b) => {
2143
+ const out = [];
2144
+ for (let c = a; c <= b; c++)
2145
+ out.push(c);
2146
+ return out;
2147
+ };
2148
+ switch (name) {
2149
+ case 'upper':
2150
+ return seq(65, 90);
2151
+ case 'lower':
2152
+ return seq(97, 122);
2153
+ case 'digit':
2154
+ return seq(48, 57);
2155
+ case 'alpha':
2156
+ return [...seq(65, 90), ...seq(97, 122)];
2157
+ case 'alnum':
2158
+ return [...seq(48, 57), ...seq(65, 90), ...seq(97, 122)];
2159
+ case 'space':
2160
+ return [32, 9, 10, 13, 12, 11];
2161
+ case 'blank':
2162
+ return [32, 9];
2163
+ case 'punct':
2164
+ return [...seq(33, 47), ...seq(58, 64), ...seq(91, 96), ...seq(123, 126)];
2165
+ case 'xdigit':
2166
+ return [...seq(48, 57), ...seq(65, 70), ...seq(97, 102)];
2167
+ case 'cntrl':
2168
+ return [...seq(0, 31), 127];
2169
+ case 'print':
2170
+ return seq(32, 126);
2171
+ case 'graph':
2172
+ return seq(33, 126);
2173
+ default:
2174
+ throw new FauxnixParseError("fauxnix: tr invalid character class '[:" + name + ":]'");
2175
+ }
2176
+ }
2177
+ function expandTrSet(set) {
2178
+ const codes = [];
2179
+ let i = 0;
2180
+ while (i < set.length) {
2181
+ if (set.startsWith('[:', i)) {
2182
+ const end = set.indexOf(':]', i + 2);
2183
+ if (end < 0)
2184
+ throw new FauxnixParseError('fauxnix: tr unterminated character class');
2185
+ codes.push(...trClassCodes(set.slice(i + 2, end)));
2186
+ i = end + 2;
2187
+ continue;
2188
+ }
2189
+ if (set[i] === '\\' && i + 1 < set.length) {
2190
+ const c = set[i + 1];
2191
+ const m = {
2192
+ n: 10, t: 9, r: 13, f: 12, v: 11, '\\': 92, a: 7, b: 8,
2193
+ };
2194
+ codes.push(m[c] ?? c.charCodeAt(0));
2195
+ i += 2;
2196
+ continue;
2197
+ }
2198
+ if (i + 2 < set.length && set[i + 1] === '-') {
2199
+ const a = set.charCodeAt(i);
2200
+ const b = set.charCodeAt(i + 2);
2201
+ if (b < a) {
2202
+ throw new FauxnixParseError("fauxnix: tr range-endpoints of '" + set.slice(i, i + 3) + "' are in reverse collating sequence order");
2203
+ }
2204
+ for (let c = a; c <= b; c++)
2205
+ codes.push(c);
2206
+ i += 3;
2207
+ continue;
2208
+ }
2209
+ codes.push(set.charCodeAt(i));
2210
+ i++;
2211
+ }
2212
+ return codes;
2213
+ }
2214
+ const tr = (args) => {
2215
+ const { flags, operandWords } = parseWords(args);
2216
+ const del = flags.has('d');
2217
+ const squeeze = flags.has('s');
2218
+ if (operandWords.length === 0) {
2219
+ throw new FauxnixParseError('fauxnix: tr missing operand');
2220
+ }
2221
+ const set1 = expandTrSet(wordToString(operandWords[0]));
2222
+ let set2 = [];
2223
+ if (operandWords.length > 1) {
2224
+ set2 = expandTrSet(wordToString(operandWords[1]));
2225
+ }
2226
+ else if (!del && !squeeze) {
2227
+ throw new FauxnixParseError("fauxnix: tr missing operand after '" + wordToString(operandWords[0]) + "'");
2228
+ }
2229
+ // squeeze applies to SET2 chars when translating or deleting, else SET1
2230
+ const sqSet = squeeze ? (operandWords.length > 1 ? set2 : set1) : [];
2231
+ const mapPairs = [];
2232
+ if (!del && set2.length > 0) {
2233
+ for (let i = 0; i < set1.length; i++) {
2234
+ mapPairs.push([set1[i], set2[Math.min(i, set2.length - 1)]]);
2235
+ }
2236
+ }
2237
+ const lines = [];
2238
+ lines.push('$fx_dl = @{}');
2239
+ if (del) {
2240
+ lines.push('foreach ($c in [char[]](' + set1.join(', ') + ')) { $fx_dl[[int]$c] = $true }');
2241
+ }
2242
+ lines.push('$fx_map = @{}');
2243
+ for (const [a, b] of mapPairs) {
2244
+ lines.push('$fx_map[' + a + '] = [char]' + b);
2245
+ }
2246
+ lines.push('$fx_sq = @{}');
2247
+ if (squeeze) {
2248
+ lines.push('foreach ($c in [char[]](' + sqSet.join(', ') + ')) { $fx_sq[[int]$c] = $true }');
2249
+ }
2250
+ lines.push('foreach ($fx_line in ' + STDIN_LINES + ') {');
2251
+ lines.push(' $fx_sb = New-Object System.Text.StringBuilder');
2252
+ lines.push(' $fx_prev = -1');
2253
+ lines.push(' foreach ($fx_ch in $fx_line.ToCharArray()) {');
2254
+ lines.push(' $fx_o = [int]$fx_ch');
2255
+ if (del)
2256
+ lines.push(' if ($fx_dl.ContainsKey($fx_o)) { continue }');
2257
+ if (!del && set2.length > 0) {
2258
+ lines.push(' if ($fx_map.ContainsKey($fx_o)) { $fx_o = [int]$fx_map[$fx_o] }');
2259
+ }
2260
+ if (squeeze) {
2261
+ lines.push(' if ($fx_sq.ContainsKey($fx_o) -and $fx_o -eq $fx_prev) { continue }');
2262
+ }
2263
+ lines.push(' [void]$fx_sb.Append([char]$fx_o)');
2264
+ lines.push(' $fx_prev = $fx_o');
2265
+ lines.push(' }');
2266
+ lines.push(' $fx_sb.ToString()');
2267
+ lines.push('}');
2268
+ lines.push('$script:fx_exit = 0');
2269
+ return lines.join('\n');
2270
+ };
2271
+ /* ------------------------------------------------------------------ */
2272
+ export const handlers = {
2273
+ grep,
2274
+ egrep: (args, ctx) => grep([[{ kind: 'Text', text: '-E' }], ...args], ctx), // egrep = grep -E
2275
+ sed,
2276
+ awk,
2277
+ sort,
2278
+ uniq,
2279
+ cut,
2280
+ tr,
2281
+ };