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,721 @@
1
+ import { wordToString } from '../ast.js';
2
+ import { parseWords, psStr } from '../registry.js';
3
+ import { exprOfWord, operandExpr } from '../translator.js';
4
+ /* ------------------------------------------------------------------ */
5
+ /* Shared PS snippets */
6
+ /* ------------------------------------------------------------------ */
7
+ /**
8
+ * Glob resolution emitted into handlers: bash expands globs before the
9
+ * command runs; PowerShell must do it explicitly. Unmatched globs stay
10
+ * literal (bash behavior → downstream "No such file or directory").
11
+ */
12
+ const PS_GLOB_FN = [
13
+ 'function fx-glob($p) {',
14
+ " if ($p -notlike '*[*?]*') { return @($p) }",
15
+ ' $m = @(Get-Item -Path $p -ErrorAction SilentlyContinue)',
16
+ ' if ($m.Count -eq 0) { return @($p) }',
17
+ ' return @($m | ForEach-Object { $_.FullName })',
18
+ '}',
19
+ ].join('\n');
20
+ /** PS helper: read a file as text, UTF-8 first then GBK fallback. */
21
+ const PS_READTEXT_FN = [
22
+ 'function fx-read($p) {',
23
+ ' $b = [IO.File]::ReadAllBytes($p)',
24
+ ' try { return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($b) }',
25
+ ' catch { try { return [System.Text.Encoding]::GetEncoding(936).GetString($b) } catch { return [System.Text.Encoding]::ASCII.GetString($b) } }',
26
+ '}',
27
+ ].join('\n');
28
+ /** PS helper: GNU-style mtime string (MMM d HH:mm, or MMM d YYYY when old). */
29
+ const PS_FTIME_FN = [
30
+ 'function fx-time($d) {',
31
+ " if ($null -eq $d) { return 'Jan 1 00:00' }",
32
+ " $m = @{1='Jan';2='Feb';3='Mar';4='Apr';5='May';6='Jun';7='Jul';8='Aug';9='Sep';10='Oct';11='Nov';12='Dec'}[[int]$d.Month]",
33
+ " $day = '{0,2}' -f $d.Day",
34
+ ' $old = (((Get-Date) - $d).TotalDays -gt 182) -or (((Get-Date) - $d).TotalDays -lt -182)',
35
+ " if ($old) { return ($m + ' ' + $day + ' ' + $d.Year) }",
36
+ " return ($m + ' ' + $day + ' ' + $d.ToString('HH:mm'))",
37
+ '}',
38
+ ].join('\n');
39
+ /** PS helper: human-readable size (GNU style: 1.5K, 2.3M...). */
40
+ const PS_HSIZE_FN = [
41
+ 'function fx-hsize($n) {',
42
+ " $u = 'B'; if ($n -ge 1GB) { $n = [math]::Round($n / 1GB, 1); $u = 'G' } elseif ($n -ge 1MB) { $n = [math]::Round($n / 1MB, 1); $u = 'M' } elseif ($n -ge 1KB) { $n = [math]::Round($n / 1KB, 1); $u = 'K' }",
43
+ " return ('{0}{1}' -f $n, $u)",
44
+ '}',
45
+ ].join('\n');
46
+ /** Operand Words → PS array expression of string exprs. */
47
+ function psArray(words, fn = operandExpr) {
48
+ if (words.length === 0)
49
+ return '@()';
50
+ return '@(' + words.map(fn).join(', ') + ')';
51
+ }
52
+ /* ------------------------------------------------------------------ */
53
+ /* ls */
54
+ /* ------------------------------------------------------------------ */
55
+ const ls = (args) => {
56
+ const { flags, longs, operandWords } = parseWords(args);
57
+ const long = flags.has('l') || longs.has('--format=long') || longs.has('--long');
58
+ const all = flags.has('a') || longs.has('--all');
59
+ const almost = flags.has('A') || longs.has('--almost-all');
60
+ const dirOnly = flags.has('d') || longs.has('--directory');
61
+ const human = flags.has('h') || longs.has('--human-readable');
62
+ const classify = flags.has('F') || flags.has('p') || longs.has('--classify');
63
+ const sortByTime = flags.has('t');
64
+ const sortBySize = flags.has('S');
65
+ const reverse = flags.has('r');
66
+ const targets = operandWords.length ? operandWords.map((w) => operandExpr(w)) : ["'.'"];
67
+ return [
68
+ PS_GLOB_FN,
69
+ PS_FTIME_FN,
70
+ 'function fx-mode($it) {',
71
+ " $t = '-'; if ($it.PSIsContainer) { $t = 'd' } elseif ($it.LinkType) { $t = 'l' }",
72
+ " if ($it.PSIsContainer) { return ($t + 'rwxr-xr-x') }",
73
+ " $ro = $it.Attributes.ToString().Contains('ReadOnly')",
74
+ ' $ex = $false',
75
+ " if (-not $it.PSIsContainer) { $ex = @('.exe','.bat','.cmd','.ps1','.com','.msi','.sh','.py') -contains $it.Extension.ToLower() }",
76
+ " if ($ro) { return ($t + 'r--r--r--') }",
77
+ " if ($ex) { return ($t + 'rwxr-xr-x') }",
78
+ " return ($t + 'rw-r--r--')",
79
+ '}',
80
+ 'function fx-name($it) {',
81
+ ' $n = $it.Name',
82
+ ' if (' + (classify ? '$true' : '$false') + ') {',
83
+ " if ($it.PSIsContainer) { $n = $n + '/' } elseif (@('.exe','.bat','.cmd','.com','.msi','.ps1') -contains $it.Extension.ToLower()) { $n = $n + '*' } elseif ($it.LinkType) { $n = $n + '@' }",
84
+ ' }',
85
+ ' return $n',
86
+ '}',
87
+ '$fx_targets = @(' + targets.join(', ') + ')',
88
+ '$fx_all = @()',
89
+ 'foreach ($fx_t in $fx_targets) {',
90
+ ' foreach ($fx_g in (fx-glob $fx_t)) {',
91
+ ' if (-not (Test-Path -LiteralPath $fx_g)) {',
92
+ ' [Console]::Error.WriteLine("ls: cannot access \'" + $fx_t + "\': No such file or directory"); $script:fx_exit = 2; continue',
93
+ ' }',
94
+ ' $fx_all += ,(Get-Item -LiteralPath $fx_g -Force)',
95
+ ' }',
96
+ '}',
97
+ '$fx_show = @()',
98
+ 'if (' + (dirOnly ? '$true' : '$false') + ') { $fx_show = $fx_all }',
99
+ 'else {',
100
+ ' foreach ($fx_it in $fx_all) {',
101
+ ' if ($fx_it.PSIsContainer) {',
102
+ ' $fx_show += @(Get-ChildItem -LiteralPath $fx_it.FullName -Force:' + (all || almost ? '$true' : '$false') + ')',
103
+ ' } else { $fx_show += ,$fx_it }',
104
+ ' }',
105
+ '}',
106
+ 'if (' + (sortByTime ? '$true' : '$false') + ') { $fx_show = @($fx_show | Sort-Object -Property LastWriteTime -Descending) }',
107
+ 'elseif (' + (sortBySize ? '$true' : '$false') + ') { $fx_show = @($fx_show | Sort-Object -Property Length -Descending) }',
108
+ 'else { $fx_show = @($fx_show | Sort-Object -Property Name) }',
109
+ 'if (' + (reverse ? '$true' : '$false') + ') { [array]::Reverse($fx_show) }',
110
+ 'foreach ($fx_it in $fx_show) {',
111
+ ' if (' + (long ? '$true' : '$false') + ') {',
112
+ ' $fx_size = 4096',
113
+ ' if (-not $fx_it.PSIsContainer) { try { $fx_size = $fx_it.Length } catch { $fx_size = 0 } }',
114
+ ' if (' + (human ? '$true' : '$false') + ') { $fx_s = fx-hsize $fx_size } else { $fx_s = [string]$fx_size }',
115
+ " '{0} 1 {1} {2} {3,13} {4} {5}' -f (fx-mode $fx_it), $env:USERNAME, $env:USERNAME, $fx_s, (fx-time $fx_it.LastWriteTime), (fx-name $fx_it)",
116
+ ' } else {',
117
+ ' fx-name $fx_it',
118
+ ' }',
119
+ '}',
120
+ ].join('\n');
121
+ };
122
+ /* ------------------------------------------------------------------ */
123
+ /* cp / mv / rm */
124
+ /* ------------------------------------------------------------------ */
125
+ const cp = (args) => {
126
+ const { flags, longs, operandWords } = parseWords(args);
127
+ const recurse = flags.has('r') || flags.has('R') || longs.has('--recursive');
128
+ const verbose = flags.has('v') || longs.has('--verbose');
129
+ const srcs = psArray(operandWords.slice(0, -1));
130
+ const dst = operandWords.length >= 2 ? operandExpr(operandWords[operandWords.length - 1]) : "''";
131
+ return [
132
+ PS_GLOB_FN,
133
+ '$fx_srcs = ' + srcs,
134
+ '$fx_dst = ' + dst,
135
+ "if ($fx_srcs.Count -eq 0) { [Console]::Error.WriteLine('cp: missing file operand'); $script:fx_exit = 1 }",
136
+ "elseif ($fx_dst -eq '') { [Console]::Error.WriteLine('cp: missing destination file operand'); $script:fx_exit = 1 }",
137
+ 'else {',
138
+ ' foreach ($fx_s in $fx_srcs) {',
139
+ ' foreach ($fx_g in (fx-glob $fx_s)) {',
140
+ ' if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine("cp: cannot stat \'" + $fx_g + "\': No such file or directory"); $script:fx_exit = 1; continue }',
141
+ ' $fx_isdir = (Test-Path -LiteralPath $fx_g -PathType Container)',
142
+ ' if ($fx_isdir -and ' + (recurse ? '$false' : '$true') + ') { [Console]::Error.WriteLine("cp: -r not specified; omitting directory \'" + $fx_g + "\'"); $script:fx_exit = 1; continue }',
143
+ ' $fx_target = $fx_dst',
144
+ ' if (Test-Path -LiteralPath $fx_dst -PathType Container) { $fx_target = Join-Path $fx_dst (Split-Path $fx_g -Leaf) }',
145
+ ' try {',
146
+ ' Copy-Item -LiteralPath $fx_g -Destination $fx_target -Recurse:' + (recurse ? '$true' : '$false') + ' -Force',
147
+ ' if (' + (verbose ? '$true' : '$false') + ') { [Console]::Error.WriteLine("\'" + $fx_g + "\' -> \'" + $fx_target + "\'") }',
148
+ ' } catch { [Console]::Error.WriteLine("cp: cannot copy \'" + $fx_g + "\': " + $_.Exception.Message); $script:fx_exit = 1 }',
149
+ ' }',
150
+ ' }',
151
+ '}',
152
+ ].join('\n');
153
+ };
154
+ const mv = (args) => {
155
+ const { flags, longs, operandWords } = parseWords(args);
156
+ const verbose = flags.has('v') || longs.has('--verbose');
157
+ const srcs = psArray(operandWords.slice(0, -1));
158
+ const dst = operandWords.length >= 2 ? operandExpr(operandWords[operandWords.length - 1]) : "''";
159
+ return [
160
+ PS_GLOB_FN,
161
+ '$fx_srcs = ' + srcs,
162
+ '$fx_dst = ' + dst,
163
+ "if ($fx_srcs.Count -eq 0) { [Console]::Error.WriteLine('mv: missing file operand'); $script:fx_exit = 1 }",
164
+ "elseif ($fx_dst -eq '') { [Console]::Error.WriteLine('mv: missing destination file operand'); $script:fx_exit = 1 }",
165
+ 'elseif ($fx_srcs.Count -gt 1 -and -not (Test-Path -LiteralPath $fx_dst -PathType Container)) { [Console]::Error.WriteLine("mv: target \'" + $fx_dst + "\' is not a directory"); $script:fx_exit = 1 }',
166
+ 'else {',
167
+ ' foreach ($fx_s in $fx_srcs) {',
168
+ ' foreach ($fx_g in (fx-glob $fx_s)) {',
169
+ ' if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine("mv: cannot stat \'" + $fx_g + "\': No such file or directory"); $script:fx_exit = 1; continue }',
170
+ ' $fx_target = $fx_dst',
171
+ ' if (Test-Path -LiteralPath $fx_dst -PathType Container) { $fx_target = Join-Path $fx_dst (Split-Path $fx_g -Leaf) }',
172
+ ' try {',
173
+ ' if (Test-Path -LiteralPath $fx_target) { Remove-Item -LiteralPath $fx_target -Recurse -Force }',
174
+ ' Move-Item -LiteralPath $fx_g -Destination $fx_target -Force',
175
+ ' if (' + (verbose ? '$true' : '$false') + ') { [Console]::Error.WriteLine("renamed \'" + $fx_g + "\' -> \'" + $fx_target + "\'") }',
176
+ ' } catch { [Console]::Error.WriteLine("mv: cannot move \'" + $fx_g + "\': " + $_.Exception.Message); $script:fx_exit = 1 }',
177
+ ' }',
178
+ ' }',
179
+ '}',
180
+ ].join('\n');
181
+ };
182
+ const rm = (args) => {
183
+ const { flags, longs, operandWords } = parseWords(args);
184
+ const recurse = flags.has('r') || flags.has('R') || longs.has('--recursive');
185
+ const force = flags.has('f') || longs.has('--force');
186
+ const verbose = flags.has('v');
187
+ return [
188
+ PS_GLOB_FN,
189
+ '$fx_files = ' + psArray(operandWords),
190
+ 'if ($fx_files.Count -eq 0 -and ' + (force ? '$false' : '$true') + ') { [Console]::Error.WriteLine(\'rm: missing operand\'); $script:fx_exit = 1 }',
191
+ 'foreach ($fx_f in $fx_files) {',
192
+ ' foreach ($fx_g in (fx-glob $fx_f)) {',
193
+ ' if (-not (Test-Path -LiteralPath $fx_g)) {',
194
+ ' if (' + (force ? '$false' : '$true') + ') { [Console]::Error.WriteLine("rm: cannot remove \'" + $fx_g + "\': No such file or directory"); $script:fx_exit = 1 }',
195
+ ' continue',
196
+ ' }',
197
+ ' $fx_isdir = (Test-Path -LiteralPath $fx_g -PathType Container)',
198
+ ' if ($fx_isdir -and ' + (recurse ? '$false' : '$true') + ') { [Console]::Error.WriteLine("rm: cannot remove \'" + $fx_g + "\': Is a directory"); $script:fx_exit = 1; continue }',
199
+ ' try {',
200
+ ' Remove-Item -LiteralPath $fx_g -Recurse:' + (recurse ? '$true' : '$false') + ' -Force',
201
+ ' if (' + (verbose ? '$true' : '$false') + ') { [Console]::Error.WriteLine("removed \'" + $fx_g + "\'") }',
202
+ ' } catch { [Console]::Error.WriteLine("rm: cannot remove \'" + $fx_g + "\': Permission denied"); $script:fx_exit = 1 }',
203
+ ' }',
204
+ '}',
205
+ ].join('\n');
206
+ };
207
+ /* ------------------------------------------------------------------ */
208
+ /* mkdir / rmdir / touch / mktemp */
209
+ /* ------------------------------------------------------------------ */
210
+ const mkdir = (args) => {
211
+ const { flags, longs, operandWords } = parseWords(args);
212
+ const parents = flags.has('p') || longs.has('--parents');
213
+ const verbose = flags.has('v');
214
+ return [
215
+ '$fx_dirs = ' + psArray(operandWords),
216
+ "if ($fx_dirs.Count -eq 0) { [Console]::Error.WriteLine('mkdir: missing operand'); $script:fx_exit = 1 }",
217
+ 'foreach ($fx_d in $fx_dirs) {',
218
+ ' if (Test-Path -LiteralPath $fx_d) {',
219
+ ' if (' + (parents ? '$false' : '$true') + ') { [Console]::Error.WriteLine("mkdir: cannot create directory \'" + $fx_d + "\': File exists"); $script:fx_exit = 1 }',
220
+ ' continue',
221
+ ' }',
222
+ ' try {',
223
+ ' New-Item -ItemType Directory -Path $fx_d -Force:' + (parents ? '$true' : '$false') + ' | Out-Null',
224
+ ' if (' + (verbose ? '$true' : '$false') + ') { [Console]::Error.WriteLine("mkdir: created directory \'" + $fx_d + "\'") }',
225
+ ' } catch { [Console]::Error.WriteLine("mkdir: cannot create directory \'" + $fx_d + "\': " + $_.Exception.Message); $script:fx_exit = 1 }',
226
+ '}',
227
+ ].join('\n');
228
+ };
229
+ const rmdir = (args) => {
230
+ const { operandWords } = parseWords(args);
231
+ return [
232
+ '$fx_dirs = ' + psArray(operandWords),
233
+ "if ($fx_dirs.Count -eq 0) { [Console]::Error.WriteLine('rmdir: missing operand'); $script:fx_exit = 1 }",
234
+ 'foreach ($fx_d in $fx_dirs) {',
235
+ ' if (-not (Test-Path -LiteralPath $fx_d -PathType Container)) { [Console]::Error.WriteLine("rmdir: failed to remove \'" + $fx_d + "\': No such file or directory"); $script:fx_exit = 1; continue }',
236
+ ' if ((Get-ChildItem -LiteralPath $fx_d -Force).Count -gt 0) { [Console]::Error.WriteLine("rmdir: failed to remove \'" + $fx_d + "\': Directory not empty"); $script:fx_exit = 1; continue }',
237
+ ' try { Remove-Item -LiteralPath $fx_d -Force } catch { [Console]::Error.WriteLine("rmdir: failed to remove \'" + $fx_d + "\': " + $_.Exception.Message); $script:fx_exit = 1 }',
238
+ '}',
239
+ ].join('\n');
240
+ };
241
+ const touch = (args) => {
242
+ const { operandWords } = parseWords(args);
243
+ return [
244
+ '$fx_files = ' + psArray(operandWords),
245
+ "if ($fx_files.Count -eq 0) { [Console]::Error.WriteLine('touch: missing file operand'); $script:fx_exit = 1 }",
246
+ 'foreach ($fx_f in $fx_files) {',
247
+ ' if (Test-Path -LiteralPath $fx_f) {',
248
+ ' try { (Get-Item -LiteralPath $fx_f).LastWriteTime = Get-Date } catch { [Console]::Error.WriteLine("touch: cannot touch \'" + $fx_f + "\': Permission denied"); $script:fx_exit = 1 }',
249
+ ' } else {',
250
+ ' try { New-Item -ItemType File -Path $fx_f | Out-Null }',
251
+ ' catch { [Console]::Error.WriteLine("touch: cannot touch \'" + $fx_f + "\': No such file or directory"); $script:fx_exit = 1 }',
252
+ ' }',
253
+ '}',
254
+ ].join('\n');
255
+ };
256
+ const mktemp = (args) => {
257
+ const { flags } = parseWords(args);
258
+ const dir = flags.has('d');
259
+ return [
260
+ 'try {',
261
+ ' if (' + (dir ? '$true' : '$false') + ') {',
262
+ " $fx_p = Join-Path $env:TEMP ('fauxnix-' + ([IO.Path]::GetRandomFileName() -replace '\\.', ''))",
263
+ ' New-Item -ItemType Directory -Path $fx_p | Out-Null',
264
+ ' } else {',
265
+ ' $fx_p = [IO.Path]::GetTempFileName()',
266
+ ' }',
267
+ ' $fx_p',
268
+ "} catch { [Console]::Error.WriteLine('mktemp: failed to create file: ' + $_.Exception.Message); $script:fx_exit = 1 }",
269
+ ].join('\n');
270
+ };
271
+ /* ------------------------------------------------------------------ */
272
+ /* ln / readlink / realpath */
273
+ /* ------------------------------------------------------------------ */
274
+ const ln = (args) => {
275
+ const { flags, operandWords } = parseWords(args);
276
+ const sym = flags.has('s');
277
+ const src = operandWords.length >= 2 ? operandExpr(operandWords[0]) : "''";
278
+ const dst = operandWords.length >= 2 ? operandExpr(operandWords[1]) : "''";
279
+ const kind = sym ? 'SymbolicLink' : 'HardLink';
280
+ const label = sym ? 'symbolic link' : 'hard link';
281
+ return [
282
+ '$fx_src = ' + src,
283
+ '$fx_dst = ' + dst,
284
+ "if ($fx_src -eq '' -or $fx_dst -eq '') { [Console]::Error.WriteLine('ln: missing file operand'); $script:fx_exit = 1 }",
285
+ 'else {',
286
+ ' if (Test-Path -LiteralPath $fx_dst -PathType Container) { $fx_dst = Join-Path $fx_dst (Split-Path $fx_src -Leaf) }',
287
+ ' try { New-Item -ItemType ' + kind + ' -Path $fx_dst -Target $fx_src | Out-Null }',
288
+ ' catch { [Console]::Error.WriteLine("ln: failed to create ' + label + ' \'" + $fx_dst + "\': " + $_.Exception.Message); $script:fx_exit = 1 }',
289
+ '}',
290
+ ].join('\n');
291
+ };
292
+ const readlink = (args) => {
293
+ const { flags, operandWords } = parseWords(args);
294
+ const canon = flags.has('f');
295
+ return [
296
+ '$fx_p = ' + (operandWords.length ? operandExpr(operandWords[0]) : "''"),
297
+ "if ($fx_p -eq '') { [Console]::Error.WriteLine('readlink: missing operand'); $script:fx_exit = 1 }",
298
+ 'elseif (-not (Test-Path -LiteralPath $fx_p)) { [Console]::Error.WriteLine("readlink: " + $fx_p + ": No such file or directory"); $script:fx_exit = 1 }',
299
+ 'else {',
300
+ ' if (' + (canon ? '$true' : '$false') + ') { (Resolve-Path -LiteralPath $fx_p).ProviderPath }',
301
+ ' else {',
302
+ ' $fx_it = Get-Item -LiteralPath $fx_p -Force',
303
+ ' if ($fx_it.LinkType) { $fx_it.Target }',
304
+ ' else { $script:fx_exit = 1 }',
305
+ ' }',
306
+ '}',
307
+ ].join('\n');
308
+ };
309
+ const realpath = (args) => {
310
+ const { operandWords } = parseWords(args);
311
+ return [
312
+ '$fx_ps = ' + psArray(operandWords),
313
+ "if ($fx_ps.Count -eq 0) { [Console]::Error.WriteLine('realpath: missing operand'); $script:fx_exit = 1 }",
314
+ 'foreach ($fx_p in $fx_ps) {',
315
+ ' if (-not (Test-Path -LiteralPath $fx_p)) { [Console]::Error.WriteLine("realpath: " + $fx_p + ": No such file or directory"); $script:fx_exit = 1; continue }',
316
+ ' (Resolve-Path -LiteralPath $fx_p).ProviderPath',
317
+ '}',
318
+ ].join('\n');
319
+ };
320
+ /* ------------------------------------------------------------------ */
321
+ /* basename / dirname */
322
+ /* ------------------------------------------------------------------ */
323
+ const basename = (args) => {
324
+ if (args.length === 2) {
325
+ return [
326
+ '$fx_p = ' + exprOfWord(args[0]),
327
+ '$fx_sfx = ' + exprOfWord(args[1]),
328
+ "$fx_n = [IO.Path]::GetFileName(($fx_p.TrimEnd('/')).TrimEnd('\\'))",
329
+ "if ($fx_n -eq '') { $fx_n = '/' }",
330
+ "if ($fx_sfx -ne '' -and $fx_n.EndsWith($fx_sfx) -and ($fx_n.Length -gt $fx_sfx.Length)) { $fx_n = $fx_n.Substring(0, $fx_n.Length - $fx_sfx.Length) }",
331
+ '$fx_n',
332
+ ].join('\n');
333
+ }
334
+ return [
335
+ '$fx_ps = @(' + args.map(exprOfWord).join(', ') + ')',
336
+ "if ($fx_ps.Count -eq 0) { [Console]::Error.WriteLine('basename: missing operand'); $script:fx_exit = 1 }",
337
+ 'foreach ($fx_p in $fx_ps) {',
338
+ " $fx_n = [IO.Path]::GetFileName(($fx_p.TrimEnd('/')).TrimEnd('\\'))",
339
+ " if ($fx_n -eq '') { $fx_n = '/' }",
340
+ ' $fx_n',
341
+ '}',
342
+ ].join('\n');
343
+ };
344
+ const dirname = (args) => {
345
+ return [
346
+ '$fx_ps = @(' + args.map(exprOfWord).join(', ') + ')',
347
+ "if ($fx_ps.Count -eq 0) { [Console]::Error.WriteLine('dirname: missing operand'); $script:fx_exit = 1 }",
348
+ 'foreach ($fx_p in $fx_ps) {',
349
+ " $fx_n = ($fx_p.TrimEnd('/')).TrimEnd('\\')",
350
+ " $fx_i = [math]::Max($fx_n.LastIndexOf('/'), $fx_n.LastIndexOf('\\'))",
351
+ " if ($fx_i -lt 0) { '.' }",
352
+ ' elseif ($fx_i -eq 0) { $fx_n.Substring(0, 1) }',
353
+ " else { $fx_n.Substring(0, $fx_i).Replace('\\', '/') }",
354
+ '}',
355
+ ].join('\n');
356
+ };
357
+ /* ------------------------------------------------------------------ */
358
+ /* stat / file */
359
+ /* ------------------------------------------------------------------ */
360
+ const stat = (args) => {
361
+ const { longs, values, operandWords } = parseWords(args, ['c'], ['--format', '--printf']);
362
+ const fmt = values.get('-c') ?? values.get('--format') ?? values.get('--printf') ?? null;
363
+ return [
364
+ PS_FTIME_FN,
365
+ PS_GLOB_FN,
366
+ '$fx_files = ' + psArray(operandWords),
367
+ "if ($fx_files.Count -eq 0) { [Console]::Error.WriteLine('stat: missing operand'); $script:fx_exit = 1 }",
368
+ 'foreach ($fx_f in $fx_files) {',
369
+ ' foreach ($fx_g in (fx-glob $fx_f)) {',
370
+ ' if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine("stat: cannot statx \'" + $fx_g + "\': No such file or directory"); $script:fx_exit = 1; continue }',
371
+ ' $fx_it = Get-Item -LiteralPath $fx_g -Force',
372
+ ' $fx_size = 0; if (-not $fx_it.PSIsContainer) { try { $fx_size = $fx_it.Length } catch {} }',
373
+ " $fx_ft = 'regular file'; if ($fx_it.PSIsContainer) { $fx_ft = 'directory' } elseif ($fx_it.LinkType) { $fx_ft = 'symbolic link' }",
374
+ " $fx_ro = $fx_it.Attributes.ToString().Contains('ReadOnly')",
375
+ " $fx_mode = '0664'; if ($fx_it.PSIsContainer) { $fx_mode = '0775' } elseif ($fx_ro) { $fx_mode = '0444' }",
376
+ " $fx_epoch = [int](($fx_it.LastWriteTime.ToUniversalTime() - [datetime]'1970-01-01').TotalSeconds)",
377
+ ' if (' + (fmt ? '$true' : '$false') + ') {',
378
+ ' $fx_o = ' + psStr(fmt ?? ''),
379
+ " $fx_o = $fx_o.Replace('%s', [string]$fx_size).Replace('%n', $fx_g).Replace('%F', $fx_ft).Replace('%a', $fx_mode).Replace('%Y', [string]$fx_epoch).Replace('%y', $fx_it.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')).Replace('%%', '%')",
380
+ ' $fx_o',
381
+ ' } else {',
382
+ ' " File: " + $fx_g',
383
+ ' (" Size: {0}`tBlocks: {1}`tIO Block: 4096 {2}" -f $fx_size, [math]::Ceiling($fx_size / 512), $fx_ft)',
384
+ ' "Device: 8h/8d`tInode: 0`tLinks: 1"',
385
+ ' ("Access: ({0}) Uid: {1} Gid: {1}" -f $fx_mode, $env:USERNAME)',
386
+ " (\"Modify: {0}.000000000 +0000\" -f $fx_it.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'))",
387
+ ' }',
388
+ ' }',
389
+ '}',
390
+ ].join('\n');
391
+ };
392
+ const file = (args) => {
393
+ const { operandWords } = parseWords(args);
394
+ return [
395
+ PS_GLOB_FN,
396
+ PS_READTEXT_FN,
397
+ '$fx_files = ' + psArray(operandWords),
398
+ "if ($fx_files.Count -eq 0) { [Console]::Error.WriteLine('file: missing operand'); $script:fx_exit = 1 }",
399
+ 'foreach ($fx_f in $fx_files) {',
400
+ ' foreach ($fx_g in (fx-glob $fx_f)) {',
401
+ ' if (-not (Test-Path -LiteralPath $fx_g)) { [Console]::Error.WriteLine($fx_g + ": cannot open (No such file or directory)"); $script:fx_exit = 1; continue }',
402
+ ' $fx_it = Get-Item -LiteralPath $fx_g -Force',
403
+ ' if ($fx_it.PSIsContainer) { "$fx_g: directory"; continue }',
404
+ ' if ($fx_it.LinkType) { "$fx_g: symbolic link to " + $fx_it.Target; continue }',
405
+ ' $fx_ext = $fx_it.Extension.ToLower()',
406
+ ' if (@(\'.exe\', \'.dll\', \'.sys\') -contains $fx_ext) { "$fx_g: PE32+ executable (console) Intel 80386, for MS Windows"; continue }',
407
+ ' $fx_bytes = [IO.File]::ReadAllBytes($fx_g)',
408
+ ' if ($fx_bytes.Length -eq 0) { "$fx_g: empty"; continue }',
409
+ ' $fx_nul = $false',
410
+ ' $fx_lim = [math]::Min(8192, $fx_bytes.Length)',
411
+ ' for ($fx_i = 0; $fx_i -lt $fx_lim; $fx_i++) { if ($fx_bytes[$fx_i] -eq 0) { $fx_nul = $true; break } }',
412
+ ' if ($fx_nul) { "$fx_g: data"; continue }',
413
+ ' $fx_txt = fx-read $fx_g',
414
+ ' if ($fx_txt.StartsWith(\'#!/\')) { "$fx_g: " + $fx_txt.Split("`n")[0].Trim() + " a /bin/sh script text executable" }',
415
+ ' else {',
416
+ ' $fx_nonascii = $false',
417
+ ' foreach ($fx_c in $fx_txt.ToCharArray()) { if ([int]$fx_c -gt 127) { $fx_nonascii = $true; break } }',
418
+ ' if ($fx_nonascii) { "$fx_g: UTF-8 Unicode text" } else { "$fx_g: ASCII text" }',
419
+ ' }',
420
+ ' }',
421
+ '}',
422
+ ].join('\n');
423
+ };
424
+ /* ------------------------------------------------------------------ */
425
+ /* du / df */
426
+ /* ------------------------------------------------------------------ */
427
+ const du = (args) => {
428
+ const { flags, longs, operandWords } = parseWords(args, [], ['--max-depth']);
429
+ const sum = flags.has('s') || longs.has('--summarize');
430
+ const human = flags.has('h') || longs.has('--human-readable');
431
+ const targets = operandWords.length ? operandWords.map((w) => operandExpr(w)) : ["'.'"];
432
+ return [
433
+ PS_HSIZE_FN,
434
+ 'function fx-size($p) {',
435
+ ' $t = 0',
436
+ ' Get-ChildItem -LiteralPath $p -Recurse -Force -File -ErrorAction SilentlyContinue | ForEach-Object { $t += $_.Length }',
437
+ ' return [math]::Ceiling($t / 1KB)',
438
+ '}',
439
+ '$fx_ts = @(' + targets.join(', ') + ')',
440
+ 'foreach ($fx_t in $fx_ts) {',
441
+ ' if (-not (Test-Path -LiteralPath $fx_t)) { [Console]::Error.WriteLine("du: cannot access \'" + $fx_t + "\': No such file or directory"); $script:fx_exit = 1; continue }',
442
+ ' if (' + (sum ? '$true' : '$false') + ') {',
443
+ ' $fx_kb = fx-size $fx_t',
444
+ ' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_t } else { "{0}`t{1}" -f $fx_kb, $fx_t }',
445
+ ' } else {',
446
+ ' $fx_root = (Get-Item -LiteralPath $fx_t -Force).FullName',
447
+ ' foreach ($fx_d in @(Get-ChildItem -LiteralPath $fx_t -Recurse -Force -Directory -ErrorAction SilentlyContinue)) {',
448
+ ' $fx_kb = fx-size $fx_d.FullName',
449
+ " $fx_rel = './' + $fx_d.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
450
+ ' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_rel } else { "{0}`t{1}" -f $fx_kb, $fx_rel }',
451
+ ' }',
452
+ ' $fx_kb = fx-size $fx_t',
453
+ ' if (' + (human ? '$true' : '$false') + ') { "{0}`t{1}" -f (fx-hsize ($fx_kb * 1KB)), $fx_t } else { "{0}`t{1}" -f $fx_kb, $fx_t }',
454
+ ' }',
455
+ '}',
456
+ ].join('\n');
457
+ };
458
+ const df = (args) => {
459
+ const { flags } = parseWords(args);
460
+ const human = flags.has('h') || flags.has('H');
461
+ return [
462
+ PS_HSIZE_FN,
463
+ '"Filesystem Size Used Avail Use% Mounted on"',
464
+ 'foreach ($fx_d in (Get-PSDrive -PSProvider FileSystem)) {',
465
+ ' if ($null -eq $fx_d.Free) { continue }',
466
+ ' $fx_used = $fx_d.Used; $fx_free = $fx_d.Free; $fx_tot = $fx_used + $fx_free',
467
+ ' if ($fx_tot -eq 0) { continue }',
468
+ ' $fx_pct = [int](100 * $fx_used / $fx_tot)',
469
+ ' if (' + (human ? '$true' : '$false') + ') {',
470
+ ' "{0,-15} {1,4} {2,4} {3,4} {4,3}% {5}" -f ($fx_d.Name + \':\'), (fx-hsize $fx_tot), (fx-hsize $fx_used), (fx-hsize $fx_free), $fx_pct, $fx_d.Root',
471
+ ' } else {',
472
+ ' "{0,-15} {1,8:d} {2,8:d} {3,8:d} {4,3}% {5}" -f ($fx_d.Name + \':\'), [math]::Floor($fx_tot / 1KB), [math]::Floor($fx_used / 1KB), [math]::Floor($fx_free / 1KB), $fx_pct, $fx_d.Root',
473
+ ' }',
474
+ '}',
475
+ ].join('\n');
476
+ };
477
+ /* ------------------------------------------------------------------ */
478
+ /* find */
479
+ /* ------------------------------------------------------------------ */
480
+ const find = (args) => {
481
+ const raw = args.map((w) => wordToString(w));
482
+ let pathEnd = 0;
483
+ while (pathEnd < raw.length &&
484
+ !raw[pathEnd].startsWith('-') &&
485
+ !['(', ')', '!', '-a', '-o'].includes(raw[pathEnd])) {
486
+ pathEnd++;
487
+ }
488
+ const pathWords = args.slice(0, pathEnd);
489
+ const preds = raw.slice(pathEnd);
490
+ if (preds.includes('-exec') || preds.includes('-execdir')) {
491
+ return ('[Console]::Error.WriteLine(' +
492
+ psStr('find: -exec is not supported by fauxnix; pipe into the command instead (e.g. `find . -name "*.log" | xargs rm`)') +
493
+ '); $script:fx_exit = 1');
494
+ }
495
+ const namePat = extractValue(preds, ['-name']);
496
+ const inamePat = extractValue(preds, ['-iname']);
497
+ const typeV = extractValue(preds, ['-type']);
498
+ const maxDepthS = extractValue(preds, ['-maxdepth']);
499
+ const minDepthS = extractValue(preds, ['-mindepth']);
500
+ const sizeExpr = extractValue(preds, ['-size']);
501
+ const mtimeExpr = extractValue(preds, ['-mtime']);
502
+ const wantDelete = preds.includes('-delete');
503
+ const paths = pathWords.length ? pathWords.map((w) => operandExpr(w)) : ["'.'"];
504
+ const conditions = [];
505
+ if (namePat !== null)
506
+ conditions.push("($fx_i.Name -like '" + likeOf(namePat) + "')");
507
+ if (inamePat !== null)
508
+ conditions.push("($fx_i.Name.ToLower() -like '" + likeOf(inamePat).toLowerCase() + "')");
509
+ if (typeV === 'f')
510
+ conditions.push('(-not $fx_i.PSIsContainer)');
511
+ if (typeV === 'd')
512
+ conditions.push('($fx_i.PSIsContainer)');
513
+ if (typeV === 'l')
514
+ conditions.push('([bool]$fx_i.LinkType)');
515
+ const cond = conditions.length ? conditions.join(' -and ') : '$true';
516
+ const sizeCond = sizeOf(sizeExpr);
517
+ const mtimeCond = mtimeOf(mtimeExpr);
518
+ return [
519
+ '$fx_paths = @(' + paths.join(', ') + ')',
520
+ 'foreach ($fx_p in $fx_paths) {',
521
+ ' if (-not (Test-Path -LiteralPath $fx_p)) { [Console]::Error.WriteLine("find: \'" + $fx_p + "\': No such file or directory"); $script:fx_exit = 1; continue }',
522
+ ' $fx_root = (Get-Item -LiteralPath $fx_p -Force).FullName',
523
+ ' $fx_all = @(Get-Item -LiteralPath $fx_p -Force)',
524
+ ' $fx_all += @(Get-ChildItem -LiteralPath $fx_p -Recurse -Force -ErrorAction SilentlyContinue)',
525
+ ' foreach ($fx_i in $fx_all) {',
526
+ " $fx_rel = $fx_i.FullName.Substring($fx_root.Length).TrimStart('\\').Replace('\\', '/')",
527
+ " if ($fx_rel -eq '') { $fx_disp = $fx_p } else { $fx_disp = ($fx_p.TrimEnd('/') + '/' + $fx_rel) }",
528
+ ' $fx_depth = 0; foreach ($fx_c in $fx_rel.ToCharArray()) { if ($fx_c -eq \'/\') { $fx_depth++ } }',
529
+ ' if ($fx_depth -lt ' + (minDepthS && /^\d+$/.test(minDepthS) ? minDepthS : '0') + ') { continue }',
530
+ maxDepthS && /^\d+$/.test(maxDepthS) ? ' if ($fx_depth -gt ' + maxDepthS + ') { continue }' : '',
531
+ ' if (-not (' + cond + ')) { continue }',
532
+ sizeCond ? ' $fx_sz = 0; if (-not $fx_i.PSIsContainer) { try { $fx_sz = $fx_i.Length } catch {} }' : '',
533
+ sizeCond ? ' if (-not (' + sizeCond + ')) { continue }' : '',
534
+ mtimeCond ? ' if (-not (' + mtimeCond + ')) { continue }' : '',
535
+ ' if (' + (wantDelete ? '$true' : '$false') + ') {',
536
+ ' try { Remove-Item -LiteralPath $fx_i.FullName -Recurse -Force -ErrorAction SilentlyContinue } catch {}',
537
+ ' } else {',
538
+ ' $fx_disp',
539
+ ' }',
540
+ ' }',
541
+ '}',
542
+ ]
543
+ .filter((l) => l !== '')
544
+ .join('\n');
545
+ };
546
+ function extractValue(preds, names) {
547
+ for (const n of names) {
548
+ const i = preds.indexOf(n);
549
+ if (i >= 0 && i + 1 < preds.length)
550
+ return preds[i + 1];
551
+ }
552
+ return null;
553
+ }
554
+ /** fnmatch glob → PowerShell -like pattern (same semantics for * and ?). */
555
+ function likeOf(glob) {
556
+ return glob.replace(/'/g, "''");
557
+ }
558
+ function sizeOf(expr) {
559
+ if (expr === null)
560
+ return null;
561
+ const m = expr.match(/^([+-]?)(\d+)([kMG]?|c)?$/);
562
+ if (!m)
563
+ return null;
564
+ const sign = m[1];
565
+ const n = parseInt(m[2], 10);
566
+ const unit = m[3] ?? '';
567
+ let bytes;
568
+ if (unit === 'c')
569
+ bytes = n;
570
+ else if (unit === 'k')
571
+ bytes = n * 1024;
572
+ else if (unit === 'M')
573
+ bytes = n * 1024 * 1024;
574
+ else if (unit === 'G')
575
+ bytes = n * 1024 * 1024 * 1024;
576
+ else
577
+ bytes = n * 512; // find default unit: 512-byte blocks
578
+ if (sign === '+')
579
+ return '$fx_sz -ge ' + bytes;
580
+ if (sign === '-')
581
+ return '$fx_sz -le ' + bytes;
582
+ return '$fx_sz -eq ' + bytes;
583
+ }
584
+ function mtimeOf(expr) {
585
+ if (expr === null)
586
+ return null;
587
+ const m = expr.match(/^([+-]?)(\d+)$/);
588
+ if (!m)
589
+ return null;
590
+ const sign = m[1];
591
+ const days = m[2];
592
+ if (sign === '+')
593
+ return '(((Get-Date) - $fx_i.LastWriteTime).TotalDays -ge ' + days + ')';
594
+ if (sign === '-')
595
+ return '(((Get-Date) - $fx_i.LastWriteTime).TotalDays -le ' + days + ')';
596
+ return '([math]::Floor(((Get-Date) - $fx_i.LastWriteTime).TotalDays) -eq ' + days + ')';
597
+ }
598
+ /* ------------------------------------------------------------------ */
599
+ /* chmod / chown */
600
+ /* ------------------------------------------------------------------ */
601
+ const chmod = (args) => {
602
+ const { operandWords } = parseWords(args);
603
+ if (operandWords.length < 2) {
604
+ return '[Console]::Error.WriteLine(\'chmod: missing operand\'); $script:fx_exit = 1';
605
+ }
606
+ const mode = wordToString(operandWords[0]);
607
+ const m = mode.match(/^([0-7]{3,4})$/);
608
+ const readOnly = m ? (parseInt(m[1].slice(-3)[0], 8) & 2) === 0 : null;
609
+ return [
610
+ '$fx_files = ' + psArray(operandWords.slice(1)),
611
+ 'foreach ($fx_f in $fx_files) {',
612
+ ' if (-not (Test-Path -LiteralPath $fx_f)) { [Console]::Error.WriteLine("chmod: cannot access \'" + $fx_f + "\': No such file or directory"); $script:fx_exit = 1; continue }',
613
+ ' try {',
614
+ ' $fx_it = Get-Item -LiteralPath $fx_f -Force',
615
+ ' if (' + (readOnly === null ? '$false' : String(readOnly)) + ') { $fx_it.Attributes = $fx_it.Attributes -bor [IO.FileAttributes]::ReadOnly }',
616
+ ' elseif (' + (readOnly === null ? '$false' : String(!readOnly)) + ') { $fx_it.Attributes = $fx_it.Attributes -band (-bnot [IO.FileAttributes]::ReadOnly) }',
617
+ ' # symbolic modes (+x, u+w ...): Windows has no exec bit — accepted as a no-op',
618
+ ' } catch { [Console]::Error.WriteLine("chmod: changing permissions of \'" + $fx_f + "\': " + $_.Exception.Message); $script:fx_exit = 1 }',
619
+ '}',
620
+ ].join('\n');
621
+ };
622
+ const chown = () => {
623
+ // Ownership is not a shell-level concept on Windows — succeed silently
624
+ // (same behavior as Git Bash's chown shim).
625
+ return '';
626
+ };
627
+ /* ------------------------------------------------------------------ */
628
+ /* diff — LCS-based, GNU normal format (+ -q, -u) */
629
+ /* ------------------------------------------------------------------ */
630
+ const diff = (args) => {
631
+ const { flags, operandWords } = parseWords(args);
632
+ const unified = flags.has('u') || flags.has('U');
633
+ const brief = flags.has('q') || flags.has('brief');
634
+ void unified;
635
+ const a = operandWords.length > 0 ? operandExpr(operandWords[0]) : "''";
636
+ const b = operandWords.length > 1 ? operandExpr(operandWords[1]) : "''";
637
+ return [
638
+ PS_READTEXT_FN,
639
+ 'function fx-dr($x, $y) { if ($x -eq $y) { return [string]$x } else { return ([string]$x) + \',\' + ([string]$y) } }',
640
+ '$fx_a = ' + a,
641
+ '$fx_b = ' + b,
642
+ "if ($fx_a -eq '' -or $fx_b -eq '') { [Console]::Error.WriteLine('diff: missing operand'); $script:fx_exit = 2 }",
643
+ 'elseif (-not (Test-Path -LiteralPath $fx_a)) { [Console]::Error.WriteLine("diff: " + $fx_a + ": No such file or directory"); $script:fx_exit = 2 }',
644
+ 'elseif (-not (Test-Path -LiteralPath $fx_b)) { [Console]::Error.WriteLine("diff: " + $fx_b + ": No such file or directory"); $script:fx_exit = 2 }',
645
+ 'else {',
646
+ ' $fx_la = @((fx-read $fx_a) -split "`r?`n")',
647
+ " if ($fx_la.Count -eq 1 -and $fx_la[0] -eq '') { $fx_la = @() }",
648
+ ' $fx_lb = @((fx-read $fx_b) -split "`r?`n")',
649
+ " if ($fx_lb.Count -eq 1 -and $fx_lb[0] -eq '') { $fx_lb = @() }",
650
+ ' $fx_same = ($fx_la.Count -eq $fx_lb.Count)',
651
+ ' if ($fx_same) { for ($fx_i = 0; $fx_i -lt $fx_la.Count; $fx_i++) { if ($fx_la[$fx_i] -ne $fx_lb[$fx_i]) { $fx_same = $false; break } } }',
652
+ ' if ($fx_same) { }',
653
+ ' elseif (' + (brief ? '$true' : '$false') + ') { "Files " + $fx_a + " and " + $fx_b + " differ"; $script:fx_exit = 1 }',
654
+ ' elseif ($fx_la.Count -gt 4000 -or $fx_lb.Count -gt 4000) { "Files " + $fx_a + " and " + $fx_b + " differ (too large for a fauxnix line diff)"; $script:fx_exit = 1 }',
655
+ ' else {',
656
+ ' $fx_n = $fx_la.Count; $fx_m = $fx_lb.Count',
657
+ ' $fx_w = $fx_m + 1',
658
+ " $fx_dp = New-Object 'int[]' (($fx_n + 1) * $fx_w)",
659
+ ' for ($fx_i = $fx_n - 1; $fx_i -ge 0; $fx_i--) {',
660
+ ' for ($fx_j = $fx_m - 1; $fx_j -ge 0; $fx_j--) {',
661
+ ' if ($fx_la[$fx_i] -eq $fx_lb[$fx_j]) { $fx_dp[($fx_i * $fx_w) + $fx_j] = $fx_dp[(($fx_i + 1) * $fx_w) + ($fx_j + 1)] + 1 }',
662
+ ' else { $fx_r = $fx_dp[(($fx_i + 1) * $fx_w) + $fx_j]; $fx_d2 = $fx_dp[($fx_i * $fx_w) + ($fx_j + 1)]; if ($fx_r -ge $fx_d2) { $fx_dp[($fx_i * $fx_w) + $fx_j] = $fx_r } else { $fx_dp[($fx_i * $fx_w) + $fx_j] = $fx_d2 } }',
663
+ ' }',
664
+ ' }',
665
+ " $fx_ops = @() # tuples: @('<op>', text, aIndex, bIndex)",
666
+ ' $fx_i = 0; $fx_j = 0',
667
+ ' while ($fx_i -lt $fx_n -and $fx_j -lt $fx_m) {',
668
+ " if ($fx_la[$fx_i] -eq $fx_lb[$fx_j]) { $fx_ops += ,@('=', $fx_la[$fx_i], $fx_i, $fx_j); $fx_i++; $fx_j++ }",
669
+ " elseif ($fx_dp[(($fx_i + 1) * $fx_w) + $fx_j] -ge $fx_dp[($fx_i * $fx_w) + ($fx_j + 1)]) { $fx_ops += ,@('-', $fx_la[$fx_i], $fx_i, $fx_j); $fx_i++ }",
670
+ " else { $fx_ops += ,@('+', $fx_lb[$fx_j], $fx_i, $fx_j); $fx_j++ }",
671
+ ' }',
672
+ " while ($fx_i -lt $fx_n) { $fx_ops += ,@('-', $fx_la[$fx_i], $fx_i, $fx_j); $fx_i++ }",
673
+ " while ($fx_j -lt $fx_m) { $fx_ops += ,@('+', $fx_lb[$fx_j], $fx_i, $fx_j); $fx_j++ }",
674
+ ' $script:fx_exit = 1',
675
+ ' $fx_k = 0',
676
+ ' while ($fx_k -lt $fx_ops.Count) {',
677
+ " if ($fx_ops[$fx_k][0] -eq '=') { $fx_k++; continue }",
678
+ ' $fx_start = $fx_k',
679
+ " while ($fx_k -lt $fx_ops.Count -and $fx_ops[$fx_k][0] -ne '=') { $fx_k++ }",
680
+ ' $fx_del = @(); $fx_add = @()',
681
+ " for ($fx_x = $fx_start; $fx_x -lt $fx_k; $fx_x++) { if ($fx_ops[$fx_x][0] -eq '-') { $fx_del += ,@($fx_ops[$fx_x][1], $fx_ops[$fx_x][2]) } else { $fx_add += ,@($fx_ops[$fx_x][1], $fx_ops[$fx_x][3]) } }",
682
+ ' $fx_a1 = 0; $fx_a2 = 0; $fx_b1 = 0; $fx_b2 = 0',
683
+ ' if ($fx_del.Count -gt 0) { $fx_a1 = $fx_del[0][1] + 1; $fx_a2 = $fx_del[$fx_del.Count - 1][1] + 1 }',
684
+ ' if ($fx_add.Count -gt 0) { $fx_b1 = $fx_add[0][1] + 1; $fx_b2 = $fx_add[$fx_add.Count - 1][1] + 1 }',
685
+ ' $fx_range = \'\'',
686
+ " if ($fx_del.Count -eq 0) { $fx_range = ([string]$fx_b1) + 'a' + (fx-dr $fx_b1 $fx_b2) }",
687
+ " elseif ($fx_add.Count -eq 0) { $fx_range = (fx-dr $fx_a1 $fx_a2) + 'd' + ([string]$fx_b1) }",
688
+ " else { $fx_range = (fx-dr $fx_a1 $fx_a2) + 'c' + (fx-dr $fx_b1 $fx_b2) }",
689
+ ' $fx_range',
690
+ " foreach ($fx_d in $fx_del) { '< ' + $fx_d[0] }",
691
+ " if ($fx_del.Count -gt 0 -and $fx_add.Count -gt 0) { '---' }",
692
+ " foreach ($fx_ad in $fx_add) { '> ' + $fx_ad[0] }",
693
+ ' }',
694
+ ' }',
695
+ '}',
696
+ ].join('\n');
697
+ };
698
+ export const handlers = {
699
+ ls,
700
+ ll: ls, // common alias
701
+ cp,
702
+ mv,
703
+ rm,
704
+ mkdir,
705
+ rmdir,
706
+ touch,
707
+ mktemp,
708
+ ln,
709
+ readlink,
710
+ realpath,
711
+ basename,
712
+ dirname,
713
+ stat,
714
+ file,
715
+ du,
716
+ df,
717
+ find,
718
+ chmod,
719
+ chown,
720
+ diff,
721
+ };