fauxnix-cli 0.2.1 → 0.4.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.
@@ -1,11 +1,38 @@
1
- import { wordToString } from '../ast.js';
1
+ import { FauxnixParseError, isUnquotedLiteral, wordToString } from '../ast.js';
2
2
  import { lookup, parseWords, psStr, registeredNames } from '../registry.js';
3
- import { exprOfWord, operandExpr, translateSimple } from '../translator.js';
3
+ import { argListExpr, exprOfWord, operandExpr, translateSimple, wrapTempEnv, encodeSetValExpr, escapeDq, translateCmdSub, normalizeLiteralPath, pathExpr, } from '../translator.js';
4
4
  import { handlers as textIoHandlers } from './text-io.js';
5
5
  /* ------------------------------------------------------------------ */
6
6
  /* Shared TS helpers */
7
7
  /* ------------------------------------------------------------------ */
8
8
  const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
+ /** Case-exact env shadow so `FOO` and `foo` can differ on Windows. */
10
+ function emitSetValPut(nameLit, valueExpr) {
11
+ const n = nameLit.replace(/'/g, "''");
12
+ return [
13
+ "fx-arrdrop '" + n + "'",
14
+ '$fx_sv = @()',
15
+ 'foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
16
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
17
+ ' if ($fx_eq -lt 1) { continue }',
18
+ " if ($fx_pair.Substring(0, $fx_eq) -cne '" + n + "') { $fx_sv += $fx_pair }",
19
+ '}',
20
+ "$fx_sv += ('" + n + "' + [string][char]61 + " + encodeSetValExpr(valueExpr) + ')',
21
+ '$env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
22
+ ].join('; ');
23
+ }
24
+ function emitSetValDelRuntime(nameVar) {
25
+ return [
26
+ 'fx-arrdrop ' + nameVar,
27
+ '$fx_sv = @()',
28
+ 'foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
29
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
30
+ ' if ($fx_eq -lt 1) { continue }',
31
+ ' if ($fx_pair.Substring(0, $fx_eq) -cne ' + nameVar + ') { $fx_sv += $fx_pair }',
32
+ '}',
33
+ '$env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
34
+ ].join('; ');
35
+ }
9
36
  /**
10
37
  * Split a Word into `NAME=rest` at the first literal '='. Returns null when
11
38
  * the word contains no '=' (bare name) or is dynamic before the '='.
@@ -37,9 +64,7 @@ function splitAssignWord(w) {
37
64
  }
38
65
  /** Text Words -> PS array expression. */
39
66
  function textArgs(words) {
40
- if (words.length === 0)
41
- return '@()';
42
- return '@(' + words.map(exprOfWord).join(', ') + ')';
67
+ return argListExpr(words, exprOfWord);
43
68
  }
44
69
  /** Drop dash-arguments, return operand words. */
45
70
  function stripFlags(args) {
@@ -103,10 +128,12 @@ const cd = (args) => {
103
128
  ].join('\n');
104
129
  }
105
130
  return [
106
- '$fx_d = ' + operandExpr(args[0]),
131
+ '$fx_ds = ' + argListExpr([args[0]]),
132
+ "if ($fx_ds.Count -ne 1) { [Console]::Error.WriteLine('bash: cd: too many arguments'); $script:fx_exit = 1 }",
133
+ 'else { $fx_d = [string]$fx_ds[0]',
107
134
  'if (-not (Test-Path -LiteralPath $fx_d)) { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 }',
108
135
  'elseif (-not (Test-Path -LiteralPath $fx_d -PathType Container)) { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": Not a directory"); $script:fx_exit = 1 }',
109
- 'else { try { Set-Location -LiteralPath $fx_d } catch { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 } }',
136
+ 'else { try { Set-Location -LiteralPath $fx_d } catch { [Console]::Error.WriteLine("bash: cd: " + $fx_d + ": No such file or directory"); $script:fx_exit = 1 } } }',
110
137
  ].join('\n');
111
138
  };
112
139
  const pwd = (args) => {
@@ -135,7 +162,24 @@ const exportCmd = (args) => {
135
162
  }
136
163
  sets.push(sp);
137
164
  }
138
- return sets.map((s) => '$env:' + s.name + ' = ' + exprOfWord(s.value)).join('\n');
165
+ return sets
166
+ .map((s) => {
167
+ const val = exprOfWord(s.value, { preserveCmdSub: true });
168
+ return ('$fx_exv = ' +
169
+ val +
170
+ '; $env:' +
171
+ s.name +
172
+ ' = $fx_exv' +
173
+ "; $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
174
+ s.name.replace(/'/g, "''") +
175
+ "' }) + '" +
176
+ s.name.replace(/'/g, "''") +
177
+ "') -join ';'); $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne '" +
178
+ s.name.replace(/'/g, "''") +
179
+ "' }) -join ';'); " +
180
+ emitSetValPut(s.name, '$fx_exv'));
181
+ })
182
+ .join('\n');
139
183
  };
140
184
  const unset = (args) => {
141
185
  const names = stripFlags(args);
@@ -146,6 +190,9 @@ const unset = (args) => {
146
190
  'foreach ($fx_n in $fx_ns) {',
147
191
  " if ($fx_n -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { [Console]::Error.WriteLine(\"bash: unset: '\" + $fx_n + \"': not a valid identifier\"); $script:fx_exit = 1; continue }",
148
192
  " Remove-Item -LiteralPath ('Env:\\' + $fx_n) -ErrorAction SilentlyContinue",
193
+ " $env:FAUXNIX_SETVARS = (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $fx_n }) -join ';')",
194
+ " $env:FAUXNIX_UNSETVARS = ((@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $fx_n }) + $fx_n) -join ';')",
195
+ ' ' + emitSetValDelRuntime('$fx_n'),
149
196
  '}',
150
197
  ].join('\n');
151
198
  };
@@ -190,11 +237,6 @@ const env = (args, ctx) => {
190
237
  break;
191
238
  }
192
239
  const lines = [];
193
- for (const u of unsets) {
194
- lines.push("Remove-Item -LiteralPath ('Env:\\' + " + psStr(u) + ') -ErrorAction SilentlyContinue');
195
- }
196
- for (const s of sets)
197
- lines.push('$env:' + s.name + ' = ' + exprOfWord(s.value));
198
240
  if (cmdIdx >= 0 && cmdIdx < args.length) {
199
241
  const cmdWords = args.slice(cmdIdx);
200
242
  lines.push(translateSimple({ kind: 'SimpleCommand', assignments: [], name: cmdWords[0], args: cmdWords.slice(1), redirects: [] }, ctx.position, ctx.hasStdin));
@@ -202,7 +244,8 @@ const env = (args, ctx) => {
202
244
  else {
203
245
  lines.push(ENV_LIST_PS);
204
246
  }
205
- return lines.join('\n');
247
+ // `env NAME=val cmd` / `env -u NAME cmd` must not leak into the session.
248
+ return wrapTempEnv(sets, lines.join('\n'), { unsets });
206
249
  };
207
250
  const printenv = (args) => {
208
251
  const names = stripFlags(args);
@@ -737,8 +780,36 @@ const colon = () => '';
737
780
  /* ------------------------------------------------------------------ */
738
781
  /* test / [ */
739
782
  /* ------------------------------------------------------------------ */
740
- const TEST_UNARY = new Set(['-e', '-f', '-d', '-r', '-w', '-x', '-s', '-z', '-n']);
741
- const TEST_BINARY = new Set(['=', '==', '!=', '-eq', '-ne', '-lt', '-le', '-gt', '-ge']);
783
+ const TEST_UNARY = new Set([
784
+ '-e',
785
+ '-a',
786
+ '-f',
787
+ '-d',
788
+ '-r',
789
+ '-w',
790
+ '-x',
791
+ '-s',
792
+ '-z',
793
+ '-n',
794
+ '-L',
795
+ '-h',
796
+ '-v',
797
+ ]);
798
+ const TEST_BINARY = new Set([
799
+ '=',
800
+ '==',
801
+ '!=',
802
+ '-eq',
803
+ '-ne',
804
+ '-lt',
805
+ '-le',
806
+ '-gt',
807
+ '-ge',
808
+ '-nt',
809
+ '-ot',
810
+ '-ef',
811
+ ]);
812
+ const TEST_BINARY_KSH = new Set([...TEST_BINARY, '=~', '>', '<']);
742
813
  const FX_TN_FN = [
743
814
  'function fx-tn($a, $b, $op) {',
744
815
  " if ($a -notmatch '^[+-]?[0-9]+$') { [Console]::Error.WriteLine('bash: [: ' + [string]$a + ': integer expression expected'); $script:fx_exit = 2; return $false }",
@@ -755,114 +826,1261 @@ const FX_TN_FN = [
755
826
  ' return $false',
756
827
  '}',
757
828
  ].join('\n');
829
+ const FX_TNK_FN = [
830
+ 'function fx-baseval($num, $base) {',
831
+ ' $b = [int]$base',
832
+ ' if ($b -lt 2 -or $b -gt 64) { throw "integer expression expected" }',
833
+ ' if ([string]$num -eq "") { throw "integer expression expected" }',
834
+ ' $v = [bigint]0',
835
+ ' foreach ($ch in ([string]$num).ToCharArray()) {',
836
+ ' $c = [int]$ch; $d = -1',
837
+ ' if ($c -ge 48 -and $c -le 57) { $d = $c - 48 }',
838
+ ' elseif ($c -ge 97 -and $c -le 122) { $d = $c - 87 }',
839
+ ' elseif ($c -ge 65 -and $c -le 90) { if ($b -le 36) { $d = $c - 55 } else { $d = $c - 29 } }',
840
+ ' elseif ($c -eq 64) { $d = 62 }',
841
+ ' elseif ($c -eq 95) { $d = 63 }',
842
+ ' if ($d -lt 0 -or $d -ge $b) { throw "integer expression expected" }',
843
+ ' $v = $v * $b + $d',
844
+ ' }',
845
+ ' return (fx-i64 ([string]$v))',
846
+ '}',
847
+ 'function fx-arith($s) {',
848
+ ' $t = [string]$s',
849
+ ' if ($t.Trim().Length -eq 0) { return 0 }',
850
+ " $t = [regex]::Replace($t, '\\b(\\d+)#([0-9A-Za-z@_]+)\\b', {",
851
+ ' param($m)',
852
+ ' return [string](fx-baseval $m.Groups[2].Value $m.Groups[1].Value)',
853
+ ' })',
854
+ " $t = [regex]::Replace($t, '0[xX][0-9A-Fa-f]+', {",
855
+ ' param($m)',
856
+ ' return [string](fx-baseval $m.Value.Substring(2) 16)',
857
+ ' })',
858
+ " $t = [regex]::Replace($t, '\\b0[0-9]+\\b', {",
859
+ ' param($m)',
860
+ " if ($m.Value -match '[89]') { throw 'integer expression expected' }",
861
+ " $fx_od = $m.Value.TrimStart('0')",
862
+ " if ($fx_od -eq '') { return '0' }",
863
+ ' return [string](fx-baseval $fx_od 8)',
864
+ ' })',
865
+ ' $script:fx_as = $t; $script:fx_ai = 0; $script:fx_skip = $false',
866
+ ' $v = fx-acomma',
867
+ ' fx-askip',
868
+ ' if ($script:fx_ai -lt $script:fx_as.Length) { throw "integer expression expected" }',
869
+ ' return $v',
870
+ '}',
871
+ 'function fx-askip {',
872
+ ' while ($script:fx_ai -lt $script:fx_as.Length -and ($script:fx_as[$script:fx_ai] -eq [char]32 -or $script:fx_as[$script:fx_ai] -eq [char]9)) { $script:fx_ai++ }',
873
+ '}',
874
+ 'function fx-aname {',
875
+ ' fx-askip',
876
+ ' if ($script:fx_ai -ge $script:fx_as.Length) { return $null }',
877
+ ' $c0 = [int]$script:fx_as[$script:fx_ai]',
878
+ ' if (-not (($c0 -ge 65 -and $c0 -le 90) -or ($c0 -ge 97 -and $c0 -le 122) -or $c0 -eq 95)) { return $null }',
879
+ ' $ns = $script:fx_ai; $script:fx_ai++',
880
+ ' while ($script:fx_ai -lt $script:fx_as.Length) {',
881
+ ' $c = [int]$script:fx_as[$script:fx_ai]',
882
+ ' if (($c -ge 48 -and $c -le 57) -or ($c -ge 65 -and $c -le 90) -or ($c -ge 97 -and $c -le 122) -or $c -eq 95) { $script:fx_ai++ } else { break }',
883
+ ' }',
884
+ ' return $script:fx_as.Substring($ns, $script:fx_ai - $ns)',
885
+ '}',
886
+ 'function fx-envset($n, $v) {',
887
+ ' $n = [string]$n',
888
+ ' $fx_ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1',
889
+ ' $nm = if ($fx_ev) { $fx_ev.Name } else { $n }',
890
+ " Set-Item -LiteralPath ('Env:' + $nm) -Value ([string][long]$v)",
891
+ ' fx-arrdrop $n',
892
+ " $env:FAUXNIX_SETVARS = ((@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) + $n) -join ';')",
893
+ " $env:FAUXNIX_UNSETVARS = (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ne '' -and $_ -cne $n }) -join ';')",
894
+ ' $fx_sv = @()',
895
+ ' foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
896
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
897
+ ' if ($fx_eq -lt 1) { continue }',
898
+ ' if ($fx_pair.Substring(0, $fx_eq) -cne $n) { $fx_sv += $fx_pair }',
899
+ ' }',
900
+ ' $fx_sv += ($n + [string][char]61 + ' +
901
+ '([string][long]$v))',
902
+ ' $env:FAUXNIX_SETVALS = ($fx_sv -join [string][char]10)',
903
+ '}',
904
+ 'function fx-aget($nm) {',
905
+ ' $fx_src = fx-setval-get $nm',
906
+ ' if ($null -eq $fx_src) {',
907
+ ' $fx_ev = Get-ChildItem Env: | Where-Object { $_.Name -ceq $nm } | Select-Object -First 1',
908
+ ' if ($fx_ev) { $fx_src = $fx_ev.Value }',
909
+ " elseif (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ceq $nm }).Count -gt 0) { $fx_src = [Environment]::GetEnvironmentVariable([string]$nm) }",
910
+ ' }',
911
+ ' if ($null -eq $fx_src) { return [long]0 }',
912
+ ' if ($null -eq $script:fx_astack) { $script:fx_astack = New-Object System.Collections.Generic.HashSet[string] }',
913
+ ' if (-not $script:fx_astack.Add([string]$nm)) { throw "expression recursion level exceeded" }',
914
+ ' $saveAs = $script:fx_as; $saveAi = $script:fx_ai; $saveSkip = $script:fx_skip',
915
+ ' try { $vv = fx-arith $fx_src }',
916
+ ' finally {',
917
+ ' $script:fx_as = $saveAs; $script:fx_ai = $saveAi; $script:fx_skip = $saveSkip',
918
+ ' [void]$script:fx_astack.Remove([string]$nm)',
919
+ ' }',
920
+ ' return [long]$vv',
921
+ '}',
922
+ 'function fx-i64($s) {',
923
+ ' $bi = [bigint]::Parse([string]$s)',
924
+ " $mod = [bigint]::Parse('18446744073709551616')",
925
+ " $half = [bigint]::Parse('9223372036854775808')",
926
+ ' $bi = $bi % $mod',
927
+ ' if ($bi -lt [bigint]0) { $bi = $bi + $mod }',
928
+ ' if ($bi -ge $half) { $bi = $bi - $mod }',
929
+ ' return [long]$bi',
930
+ '}',
931
+ 'function fx-iadd($a, $b) { return (fx-i64 ([string](([bigint][long]$a) + ([bigint][long]$b)))) }',
932
+ 'function fx-isub($a, $b) { return (fx-i64 ([string](([bigint][long]$a) - ([bigint][long]$b)))) }',
933
+ 'function fx-imul($a, $b) { return (fx-i64 ([string](([bigint][long]$a) * ([bigint][long]$b)))) }',
934
+ 'function fx-ineg($v) {',
935
+ ' $v = [long]$v',
936
+ ' if ($v -eq [long]::MinValue) { return $v }',
937
+ ' return [long](-$v)',
938
+ '}',
939
+ 'function fx-anum {',
940
+ ' fx-askip',
941
+ ' if ($script:fx_ai -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]33) {',
942
+ ' $script:fx_ai++',
943
+ ' $v = fx-anum',
944
+ ' if ($v -eq 0) { return 1 }',
945
+ ' return 0',
946
+ ' }',
947
+ ' if ($script:fx_ai -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]126) {',
948
+ ' $script:fx_ai++',
949
+ ' return (fx-ineg (-bnot [long](fx-anum)))',
950
+ ' }',
951
+ ' if ($script:fx_ai + 1 -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]43 -and $script:fx_as[$script:fx_ai + 1] -eq [char]43) {',
952
+ ' $save = $script:fx_ai; $script:fx_ai += 2',
953
+ ' $nm = fx-aname',
954
+ ' if ($nm) {',
955
+ ' if ($script:fx_skip) { return 0 }',
956
+ ' $cur = fx-iadd (fx-aget $nm) 1; fx-envset $nm $cur; return $cur',
957
+ ' }',
958
+ ' $script:fx_ai = $save',
959
+ ' }',
960
+ ' if ($script:fx_ai + 1 -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]45 -and $script:fx_as[$script:fx_ai + 1] -eq [char]45) {',
961
+ ' $save = $script:fx_ai; $script:fx_ai += 2',
962
+ ' $nm = fx-aname',
963
+ ' if ($nm) {',
964
+ ' if ($script:fx_skip) { return 0 }',
965
+ ' $cur = fx-isub (fx-aget $nm) 1; fx-envset $nm $cur; return $cur',
966
+ ' }',
967
+ ' $script:fx_ai = $save',
968
+ ' }',
969
+ ' if ($script:fx_ai -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]43) {',
970
+ ' $script:fx_ai++',
971
+ ' return [long](fx-anum)',
972
+ ' }',
973
+ ' if ($script:fx_ai -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]45) {',
974
+ ' $script:fx_ai++',
975
+ ' return (fx-ineg (fx-anum))',
976
+ ' }',
977
+ ' fx-askip',
978
+ ' if ($script:fx_ai -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]40) {',
979
+ ' $script:fx_ai++',
980
+ ' $v = fx-acomma',
981
+ ' fx-askip',
982
+ ' if ($script:fx_ai -ge $script:fx_as.Length -or $script:fx_as[$script:fx_ai] -ne [char]41) { throw "integer expression expected" }',
983
+ ' $script:fx_ai++',
984
+ ' return [long]$v',
985
+ ' }',
986
+ ' $nm = fx-aname',
987
+ ' if ($nm) {',
988
+ ' fx-askip',
989
+ ' $post = 0',
990
+ ' if ($script:fx_ai + 1 -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]43 -and $script:fx_as[$script:fx_ai + 1] -eq [char]43) { $script:fx_ai += 2; $post = 1 }',
991
+ ' elseif ($script:fx_ai + 1 -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]45 -and $script:fx_as[$script:fx_ai + 1] -eq [char]45) { $script:fx_ai += 2; $post = -1 }',
992
+ ' if ($script:fx_skip) { return 0 }',
993
+ ' $vv = fx-aget $nm',
994
+ ' if ($post -ne 0) { fx-envset $nm (fx-iadd $vv $post) }',
995
+ ' return [long]$vv',
996
+ ' }',
997
+ ' $start = $script:fx_ai',
998
+ ' while ($script:fx_ai -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -ge [char]48 -and $script:fx_as[$script:fx_ai] -le [char]57) { $script:fx_ai++ }',
999
+ ' if ($script:fx_ai -eq $start) { throw "integer expression expected" }',
1000
+ ' return (fx-i64 $script:fx_as.Substring($start, $script:fx_ai - $start))',
1001
+ '}',
1002
+ 'function fx-apow {',
1003
+ ' $v = fx-anum',
1004
+ ' fx-askip',
1005
+ ' if ($script:fx_ai + 1 -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai] -eq [char]42 -and $script:fx_as[$script:fx_ai + 1] -eq [char]42) {',
1006
+ ' $script:fx_ai += 2',
1007
+ ' $r = fx-apow',
1008
+ ' if ($r -lt 0) { throw "exponent less than 0" }',
1009
+ ' $fx_b = [long]$v; $fx_e = [long]$r; $fx_p = [long]1',
1010
+ ' while ($fx_e -gt 0) { if ($fx_e -band 1) { $fx_p = fx-imul $fx_p $fx_b }; $fx_b = fx-imul $fx_b $fx_b; $fx_e = $fx_e -shr 1 }',
1011
+ ' return $fx_p',
1012
+ ' }',
1013
+ ' return $v',
1014
+ '}',
1015
+ 'function fx-adiv($a, $b) {',
1016
+ ' $a = [long]$a; $b = [long]$b',
1017
+ ' if ($b -eq 0) { throw "division by 0" }',
1018
+ ' if ($a -eq [long]::MinValue -and $b -eq -1) { return [long]::MinValue }',
1019
+ ' $rem = [long]0',
1020
+ ' return [math]::DivRem($a, $b, [ref]$rem)',
1021
+ '}',
1022
+ 'function fx-amod($a, $b) {',
1023
+ ' $a = [long]$a; $b = [long]$b',
1024
+ ' if ($b -eq 0) { throw "division by 0" }',
1025
+ ' if ($a -eq [long]::MinValue -and $b -eq -1) { return [long]0 }',
1026
+ ' $rem = [long]0',
1027
+ ' [void][math]::DivRem($a, $b, [ref]$rem)',
1028
+ ' return [long]$rem',
1029
+ '}',
1030
+ 'function fx-amul {',
1031
+ ' $v = fx-apow',
1032
+ ' for (;;) {',
1033
+ ' fx-askip',
1034
+ ' if ($script:fx_ai -ge $script:fx_as.Length) { break }',
1035
+ ' $op = $script:fx_as[$script:fx_ai]',
1036
+ ' if ($op -ne [char]42 -and $op -ne [char]47 -and $op -ne [char]37) { break }',
1037
+ ' $script:fx_ai++',
1038
+ ' $r = fx-apow',
1039
+ ' if ($op -eq [char]42) { $v = fx-imul $v $r }',
1040
+ ' elseif ($op -eq [char]47) { if ($r -eq 0 -and $script:fx_skip) { $v = 0 } else { $v = fx-adiv $v $r } }',
1041
+ ' else { if ($r -eq 0 -and $script:fx_skip) { $v = 0 } else { $v = fx-amod $v $r } }',
1042
+ ' }',
1043
+ ' return $v',
1044
+ '}',
1045
+ 'function fx-aadd {',
1046
+ ' $v = fx-amul',
1047
+ ' for (;;) {',
1048
+ ' fx-askip',
1049
+ ' if ($script:fx_ai -ge $script:fx_as.Length) { break }',
1050
+ ' $op = $script:fx_as[$script:fx_ai]',
1051
+ ' if ($op -ne [char]43 -and $op -ne [char]45) { break }',
1052
+ ' $script:fx_ai++',
1053
+ ' $r = fx-amul',
1054
+ ' if ($op -eq [char]43) { $v = fx-iadd $v $r } else { $v = fx-isub $v $r }',
1055
+ ' }',
1056
+ ' return $v',
1057
+ '}',
1058
+ 'function fx-ashift {',
1059
+ ' $v = fx-aadd',
1060
+ ' for (;;) {',
1061
+ ' fx-askip',
1062
+ ' if ($script:fx_ai + 1 -ge $script:fx_as.Length) { break }',
1063
+ ' $a = $script:fx_as[$script:fx_ai]; $b = $script:fx_as[$script:fx_ai + 1]',
1064
+ ' if ($a -eq [char]60 -and $b -eq [char]60) {',
1065
+ ' $script:fx_ai += 2; $r = fx-aadd; $v = fx-i64 ([string](([bigint][long]$v) -shl [int]$r))',
1066
+ ' } elseif ($a -eq [char]62 -and $b -eq [char]62) {',
1067
+ ' $script:fx_ai += 2; $r = fx-aadd; $v = [long]$v -shr [int]$r',
1068
+ ' } else { break }',
1069
+ ' }',
1070
+ ' return $v',
1071
+ '}',
1072
+ 'function fx-arel {',
1073
+ ' $v = fx-ashift',
1074
+ ' for (;;) {',
1075
+ ' fx-askip',
1076
+ ' if ($script:fx_ai -ge $script:fx_as.Length) { break }',
1077
+ ' $a = $script:fx_as[$script:fx_ai]',
1078
+ ' $b = if ($script:fx_ai + 1 -lt $script:fx_as.Length) { $script:fx_as[$script:fx_ai + 1] } else { [char]0 }',
1079
+ ' if ($a -eq [char]60 -and $b -eq [char]61) { $script:fx_ai += 2; $r = fx-ashift; $v = [long]($v -le $r) }',
1080
+ ' elseif ($a -eq [char]62 -and $b -eq [char]61) { $script:fx_ai += 2; $r = fx-ashift; $v = [long]($v -ge $r) }',
1081
+ ' elseif ($a -eq [char]60 -and $b -ne [char]60) { $script:fx_ai++; $r = fx-ashift; $v = [long]($v -lt $r) }',
1082
+ ' elseif ($a -eq [char]62 -and $b -ne [char]62) { $script:fx_ai++; $r = fx-ashift; $v = [long]($v -gt $r) }',
1083
+ ' else { break }',
1084
+ ' }',
1085
+ ' return $v',
1086
+ '}',
1087
+ 'function fx-aeq {',
1088
+ ' $v = fx-arel',
1089
+ ' for (;;) {',
1090
+ ' fx-askip',
1091
+ ' if ($script:fx_ai + 1 -ge $script:fx_as.Length) { break }',
1092
+ ' $a = $script:fx_as[$script:fx_ai]; $b = $script:fx_as[$script:fx_ai + 1]',
1093
+ ' if ($a -eq [char]61 -and $b -eq [char]61) { $script:fx_ai += 2; $r = fx-arel; $v = [long]($v -eq $r) }',
1094
+ ' elseif ($a -eq [char]33 -and $b -eq [char]61) { $script:fx_ai += 2; $r = fx-arel; $v = [long]($v -ne $r) }',
1095
+ ' else { break }',
1096
+ ' }',
1097
+ ' return $v',
1098
+ '}',
1099
+ 'function fx-abitand {',
1100
+ ' $v = fx-aeq',
1101
+ ' for (;;) {',
1102
+ ' fx-askip',
1103
+ ' if ($script:fx_ai -ge $script:fx_as.Length) { break }',
1104
+ ' if ($script:fx_as[$script:fx_ai] -ne [char]38) { break }',
1105
+ ' if ($script:fx_ai + 1 -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai + 1] -eq [char]38) { break }',
1106
+ ' $script:fx_ai++',
1107
+ ' $v = [long]$v -band [long](fx-aeq)',
1108
+ ' }',
1109
+ ' return $v',
1110
+ '}',
1111
+ 'function fx-abitxor {',
1112
+ ' $v = fx-abitand',
1113
+ ' for (;;) {',
1114
+ ' fx-askip',
1115
+ ' if ($script:fx_ai -ge $script:fx_as.Length -or $script:fx_as[$script:fx_ai] -ne [char]94) { break }',
1116
+ ' $script:fx_ai++',
1117
+ ' $v = [long]$v -bxor [long](fx-abitand)',
1118
+ ' }',
1119
+ ' return $v',
1120
+ '}',
1121
+ 'function fx-abitor {',
1122
+ ' $v = fx-abitxor',
1123
+ ' for (;;) {',
1124
+ ' fx-askip',
1125
+ ' if ($script:fx_ai -ge $script:fx_as.Length -or $script:fx_as[$script:fx_ai] -ne [char]124) { break }',
1126
+ ' if ($script:fx_ai + 1 -lt $script:fx_as.Length -and $script:fx_as[$script:fx_ai + 1] -eq [char]124) { break }',
1127
+ ' $script:fx_ai++',
1128
+ ' $v = [long]$v -bor [long](fx-abitxor)',
1129
+ ' }',
1130
+ ' return $v',
1131
+ '}',
1132
+ 'function fx-aland {',
1133
+ ' $v = fx-abitor',
1134
+ ' for (;;) {',
1135
+ ' fx-askip',
1136
+ ' if ($script:fx_ai + 1 -ge $script:fx_as.Length) { break }',
1137
+ ' if ($script:fx_as[$script:fx_ai] -ne [char]38 -or $script:fx_as[$script:fx_ai + 1] -ne [char]38) { break }',
1138
+ ' $script:fx_ai += 2',
1139
+ ' $old = $script:fx_skip',
1140
+ ' if ($v -eq 0) { $script:fx_skip = $true }',
1141
+ ' $r = fx-abitor',
1142
+ ' $script:fx_skip = $old',
1143
+ ' $v = [long](($v -ne 0) -and ($r -ne 0))',
1144
+ ' }',
1145
+ ' return $v',
1146
+ '}',
1147
+ 'function fx-alor {',
1148
+ ' $v = fx-aland',
1149
+ ' for (;;) {',
1150
+ ' fx-askip',
1151
+ ' if ($script:fx_ai + 1 -ge $script:fx_as.Length) { break }',
1152
+ ' if ($script:fx_as[$script:fx_ai] -ne [char]124 -or $script:fx_as[$script:fx_ai + 1] -ne [char]124) { break }',
1153
+ ' $script:fx_ai += 2',
1154
+ ' $old = $script:fx_skip',
1155
+ ' if ($v -ne 0) { $script:fx_skip = $true }',
1156
+ ' $r = fx-aland',
1157
+ ' $script:fx_skip = $old',
1158
+ ' $v = [long](($v -ne 0) -or ($r -ne 0))',
1159
+ ' }',
1160
+ ' return $v',
1161
+ '}',
1162
+ 'function fx-atern {',
1163
+ ' $v = fx-alor',
1164
+ ' fx-askip',
1165
+ ' if ($script:fx_ai -ge $script:fx_as.Length -or $script:fx_as[$script:fx_ai] -ne [char]63) { return $v }',
1166
+ ' $script:fx_ai++',
1167
+ ' $old = $script:fx_skip',
1168
+ ' if ($v -eq 0) { $script:fx_skip = $true }',
1169
+ ' $t = fx-acomma',
1170
+ ' $script:fx_skip = $old',
1171
+ ' fx-askip',
1172
+ ' if ($script:fx_ai -ge $script:fx_as.Length -or $script:fx_as[$script:fx_ai] -ne [char]58) { throw "integer expression expected" }',
1173
+ ' $script:fx_ai++',
1174
+ ' if ($v -ne 0) { $script:fx_skip = $true }',
1175
+ ' $f = fx-atern',
1176
+ ' $script:fx_skip = $old',
1177
+ ' if ($v -ne 0) { return $t }',
1178
+ ' return $f',
1179
+ '}',
1180
+ 'function fx-aop {',
1181
+ ' fx-askip',
1182
+ ' if ($script:fx_ai -ge $script:fx_as.Length) { return $null }',
1183
+ ' $rest = $script:fx_as.Substring($script:fx_ai)',
1184
+ " if ($rest.StartsWith('==', [System.StringComparison]::Ordinal)) { return $null }",
1185
+ " foreach ($op in @('<<=', '>>=', '*=', '/=', '%=', '+=', '-=', '&=', '^=', '|=', '=')) {",
1186
+ ' if (-not $rest.StartsWith($op, [System.StringComparison]::Ordinal)) { continue }',
1187
+ ' $script:fx_ai += $op.Length',
1188
+ ' return $op',
1189
+ ' }',
1190
+ ' return $null',
1191
+ '}',
1192
+ 'function fx-aassign {',
1193
+ ' fx-askip',
1194
+ ' $save = $script:fx_ai',
1195
+ ' $nm = fx-aname',
1196
+ ' if ($nm) {',
1197
+ ' $op = fx-aop',
1198
+ ' if ($op) {',
1199
+ ' $old = [long]0',
1200
+ " if (-not $script:fx_skip -and $op -ne '=') { $old = [long](fx-aget $nm) }",
1201
+ ' $r = [long](fx-aassign)',
1202
+ ' if ($script:fx_skip) { return 0 }',
1203
+ ' switch ($op) {',
1204
+ " '=' { $v = [long]$r }",
1205
+ " '+=' { $v = fx-iadd $old $r }",
1206
+ " '-=' { $v = fx-isub $old $r }",
1207
+ " '*=' { $v = fx-imul $old $r }",
1208
+ " '/=' { $v = fx-adiv $old $r }",
1209
+ " '%=' { $v = fx-amod $old $r }",
1210
+ " '<<=' { $v = fx-i64 ([string](([bigint][long]$old) -shl [int]$r)) }",
1211
+ " '>>=' { $v = [long]$old -shr [int]$r }",
1212
+ " '&=' { $v = [long]$old -band [long]$r }",
1213
+ " '^=' { $v = [long]$old -bxor [long]$r }",
1214
+ " '|=' { $v = [long]$old -bor [long]$r }",
1215
+ ' default { throw "integer expression expected" }',
1216
+ ' }',
1217
+ ' fx-envset $nm $v',
1218
+ ' return [long]$v',
1219
+ ' }',
1220
+ ' }',
1221
+ ' $script:fx_ai = $save',
1222
+ ' return [long](fx-atern)',
1223
+ '}',
1224
+ 'function fx-acomma {',
1225
+ ' $v = fx-aassign',
1226
+ ' for (;;) {',
1227
+ ' fx-askip',
1228
+ ' if ($script:fx_ai -ge $script:fx_as.Length -or $script:fx_as[$script:fx_ai] -ne [char]44) { break }',
1229
+ ' $script:fx_ai++',
1230
+ ' $v = fx-aassign',
1231
+ ' }',
1232
+ ' return [long]$v',
1233
+ '}',
1234
+ 'function fx-tnk($a, $b, $op) {',
1235
+ ' try { $fx_x = fx-arith $a }',
1236
+ " catch { [Console]::Error.WriteLine('bash: [[: ' + [string]$a + ': integer expression expected'); $script:fx_exit = 1; return $false }",
1237
+ ' try { $fx_y = fx-arith $b }',
1238
+ " catch { [Console]::Error.WriteLine('bash: [[: ' + [string]$b + ': integer expression expected'); $script:fx_exit = 1; return $false }",
1239
+ ' switch ($op) {',
1240
+ " '-eq' { return ($fx_x -eq $fx_y) }",
1241
+ " '-ne' { return ($fx_x -ne $fx_y) }",
1242
+ " '-lt' { return ($fx_x -lt $fx_y) }",
1243
+ " '-le' { return ($fx_x -le $fx_y) }",
1244
+ " '-gt' { return ($fx_x -gt $fx_y) }",
1245
+ " '-ge' { return ($fx_x -ge $fx_y) }",
1246
+ ' }',
1247
+ ' return $false',
1248
+ '}',
1249
+ ].join('\n');
758
1250
  function strNe(e) {
759
1251
  return "([string](" + e + ") -ne '')";
760
1252
  }
1253
+ const FX_ISLINK_FN = [
1254
+ 'function fx-islink($p) {',
1255
+ ' try {',
1256
+ ' $fx_li = Get-Item -LiteralPath ([string]$p) -Force -ErrorAction Stop',
1257
+ ' return [bool]($fx_li.Attributes -band [IO.FileAttributes]::ReparsePoint)',
1258
+ ' } catch { return $false }',
1259
+ '}',
1260
+ ].join('\n');
1261
+ const FX_ENVGET_FN = [
1262
+ 'function fx-envexact($n) {',
1263
+ ' return (Get-ChildItem Env: | Where-Object { $_.Name -ceq ([string]$n) } | Select-Object -First 1)',
1264
+ '}',
1265
+ 'function fx-svdec($s) {',
1266
+ ' $s = [string]$s',
1267
+ ' $sb = New-Object System.Text.StringBuilder',
1268
+ ' $i = 0',
1269
+ ' while ($i -lt $s.Length) {',
1270
+ ' $c = $s[$i]',
1271
+ ' if ($c -eq [char]92 -and ($i + 1) -lt $s.Length) {',
1272
+ ' $n2 = $s[$i + 1]',
1273
+ ' if ($n2 -eq [char]110) { [void]$sb.Append([char]10); $i += 2; continue }',
1274
+ ' if ($n2 -eq [char]114) { [void]$sb.Append([char]13); $i += 2; continue }',
1275
+ ' if ($n2 -eq [char]92) { [void]$sb.Append([char]92); $i += 2; continue }',
1276
+ ' }',
1277
+ ' [void]$sb.Append($c)',
1278
+ ' $i++',
1279
+ ' }',
1280
+ ' return [string]$sb',
1281
+ '}',
1282
+ 'function fx-setval-get($n) {',
1283
+ ' foreach ($fx_pair in @($env:FAUXNIX_SETVALS -split [string][char]10)) {',
1284
+ ' $fx_eq = $fx_pair.IndexOf([char]61)',
1285
+ ' if ($fx_eq -lt 1) { continue }',
1286
+ ' if ($fx_pair.Substring(0, $fx_eq) -ceq [string]$n) { return (fx-svdec $fx_pair.Substring($fx_eq + 1)) }',
1287
+ ' }',
1288
+ ' return $null',
1289
+ '}',
1290
+ 'function fx-envexplicit($n) {',
1291
+ ' $fx_sv = fx-setval-get $n',
1292
+ ' if ($null -ne $fx_sv) { return [string]$fx_sv }',
1293
+ ' $fx_ev = fx-envexact $n',
1294
+ ' if ($fx_ev) { return [string]$fx_ev.Value }',
1295
+ // Windows preserves the provider entry's original casing (notably Path),
1296
+ // even after `$env:PATH = ...`. SETVARS is the case-sensitive proof that
1297
+ // this shell explicitly wrote the requested Bash name, so only here is a
1298
+ // case-insensitive process-environment fallback semantically safe.
1299
+ ' $fx_v = [Environment]::GetEnvironmentVariable([string]$n)',
1300
+ " if ($null -eq $fx_v) { return '' }",
1301
+ ' return [string]$fx_v',
1302
+ '}',
1303
+ 'function fx-home {',
1304
+ " if (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ceq 'HOME' }).Count -gt 0) { return (fx-envexplicit 'HOME') }",
1305
+ " if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq 'HOME' }).Count -gt 0) { return [string]$HOME }",
1306
+ ' return [string]$HOME',
1307
+ '}',
1308
+ 'function fx-envget($n) {',
1309
+ ' $n = [string]$n',
1310
+ " if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return '' }",
1311
+ " if (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return (fx-envexplicit $n) }",
1312
+ " if ($n -ceq 'HOME') { return [string]$HOME }",
1313
+ " if ($n -ceq 'PWD') { return [string]$PWD.Path }",
1314
+ " if ($n -ceq 'USER' -or $n -ceq 'LOGNAME') { return [string]$env:USERNAME }",
1315
+ " if ($n -ceq 'PATH') { return [string]$env:PATH }",
1316
+ " if ($n -ceq 'SHELL') { return 'powershell' }",
1317
+ " if ($n -ceq 'TERM') { return 'xterm-256color' }",
1318
+ " if ($n -ceq 'OLDPWD') { return [string]$env:FAUXNIX_OLDPWD }",
1319
+ " if ($n -ceq '?') { return [string]$fx_prev }",
1320
+ " if ($n -ceq '$') { return [string]$PID }",
1321
+ " if ($n -ceq 'HOSTNAME') { return [string]$env:COMPUTERNAME }",
1322
+ ' $fx_ev = fx-envexact $n',
1323
+ " if (-not $fx_ev) { return '' }",
1324
+ ' return [string]$fx_ev.Value',
1325
+ '}',
1326
+ ].join('\n');
1327
+ /** Like exprOfWord, but $var is an exact-case env lookup (bash, not $env:). */
1328
+ function kshExprOfWord(w) {
1329
+ const expanded = [];
1330
+ let tilde = false;
1331
+ if (w.length > 0 &&
1332
+ w[0].kind === 'Text' &&
1333
+ !w[0].escaped &&
1334
+ w[0].text.startsWith('~')) {
1335
+ tilde = true;
1336
+ const rest = w[0].text.slice(1);
1337
+ if (rest)
1338
+ expanded.push({ kind: 'Text', text: rest });
1339
+ expanded.push(...w.slice(1));
1340
+ }
1341
+ else {
1342
+ expanded.push(...w);
1343
+ }
1344
+ if (tilde && expanded.length === 0)
1345
+ return '(fx-home)';
1346
+ if (!tilde && expanded.length === 1 && expanded[0].kind === 'Var') {
1347
+ if (expanded[0].index !== undefined) {
1348
+ return '(fx-subget ' + psStr(expanded[0].name) + ' ' + psStr(expanded[0].index) + ')';
1349
+ }
1350
+ return '(fx-envget ' + psStr(expanded[0].name) + ')';
1351
+ }
1352
+ const literal = !tilde && expanded.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted');
1353
+ if (literal) {
1354
+ const text = expanded.map((p) => p.text).join('');
1355
+ return pathExpr(normalizeLiteralPath(text));
1356
+ }
1357
+ let out = '"';
1358
+ if (tilde)
1359
+ out += '$(fx-home)';
1360
+ const emitPart = (p) => {
1361
+ switch (p.kind) {
1362
+ case 'Text':
1363
+ case 'SingleQuoted':
1364
+ out += escapeDq(String(p.text));
1365
+ break;
1366
+ case 'DoubleQuoted':
1367
+ for (const q of p.parts)
1368
+ emitPart(q);
1369
+ break;
1370
+ case 'Var':
1371
+ out +=
1372
+ p.index !== undefined
1373
+ ? '$(fx-subget ' + psStr(p.name) + ' ' + psStr(p.index) + ')'
1374
+ : '$(fx-envget ' + psStr(p.name) + ')';
1375
+ break;
1376
+ case 'CmdSub':
1377
+ // [[ ]] does not IFS-split, so keep the newline contract.
1378
+ out += '$(' + translateCmdSub(p.cmd, true) + ')';
1379
+ break;
1380
+ }
1381
+ };
1382
+ for (const p of expanded)
1383
+ emitPart(p);
1384
+ out += '"';
1385
+ return out;
1386
+ }
1387
+ const FX_ISSET_FN = [
1388
+ 'function fx-isset($n) {',
1389
+ ' $n = [string]$n',
1390
+ " if ($n -eq '') { return $false }",
1391
+ " if ($n -match '^([A-Za-z_][A-Za-z0-9_]*)\\[([0-9]+|@|\\*)\\]$') {",
1392
+ ' $fx_ib = $Matches[1]; $fx_ix = [string]$Matches[2]',
1393
+ " if ($fx_ix -eq '@' -or $fx_ix -eq '*' -or $fx_ix -eq '0') { $n = $fx_ib }",
1394
+ ' else {',
1395
+ ' $fx_ia = @(fx-arrload $fx_ib)',
1396
+ ' $fx_ii = 0',
1397
+ ' if (-not [int]::TryParse($fx_ix, [ref]$fx_ii)) { return $false }',
1398
+ ' return ($fx_ii -ge 0 -and $fx_ii -lt $fx_ia.Count)',
1399
+ ' }',
1400
+ ' }',
1401
+ " elseif ($n -match '^[A-Za-z_][A-Za-z0-9_]*\\[') { return $false }",
1402
+ " if (@($env:FAUXNIX_UNSETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $false }",
1403
+ " if (@($env:FAUXNIX_SETVARS -split ';' | Where-Object { $_ -ceq $n }).Count -gt 0) { return $true }",
1404
+ " if (@('HOME','PWD','USER','LOGNAME','PATH','SHELL','TERM','HOSTNAME') | Where-Object { $_ -ceq $n }) { return $true }",
1405
+ " if ($n -ceq 'OLDPWD') { return [bool]$env:FAUXNIX_OLDPWD }",
1406
+ " return [bool](Get-ChildItem Env: | Where-Object { $_.Name -ceq $n } | Select-Object -First 1)",
1407
+ '}',
1408
+ ].join('\n');
1409
+ function testVExpr(w) {
1410
+ return '(fx-isset ([string](' + kshExprOfWord(w) + ')))';
1411
+ }
761
1412
  function testUnaryExpr(op, w) {
762
1413
  switch (op) {
763
1414
  case '-e':
1415
+ case '-a':
764
1416
  case '-r':
765
1417
  case '-w':
766
1418
  case '-x':
767
- return '(Test-Path -LiteralPath ' + operandExpr(w) + ')';
1419
+ return '(Test-Path -LiteralPath ' + kshExprOfWord(w) + ')';
768
1420
  case '-f':
769
- return '(Test-Path -LiteralPath ' + operandExpr(w) + ' -PathType Leaf)';
1421
+ return '(Test-Path -LiteralPath ' + kshExprOfWord(w) + ' -PathType Leaf)';
770
1422
  case '-d':
771
- return '(Test-Path -LiteralPath ' + operandExpr(w) + ' -PathType Container)';
1423
+ return '(Test-Path -LiteralPath ' + kshExprOfWord(w) + ' -PathType Container)';
1424
+ case '-L':
1425
+ case '-h':
1426
+ return '(fx-islink ' + kshExprOfWord(w) + ')';
1427
+ case '-v':
1428
+ return testVExpr(w);
772
1429
  case '-s':
773
1430
  return ('((Test-Path -LiteralPath ' +
774
- operandExpr(w) +
1431
+ kshExprOfWord(w) +
775
1432
  ' -PathType Leaf) -and ((Get-Item -LiteralPath ' +
776
- operandExpr(w) +
1433
+ kshExprOfWord(w) +
777
1434
  ' -Force).Length -gt 0))');
778
1435
  case '-z':
779
- return "([string](" + exprOfWord(w) + ") -eq '')";
1436
+ return "([string](" + kshExprOfWord(w) + ") -eq '')";
780
1437
  case '-n':
781
1438
  default:
782
- return strNe(exprOfWord(w));
1439
+ return strNe(kshExprOfWord(w));
783
1440
  }
784
1441
  }
785
- function testBinaryExpr(l, op, r) {
786
- const le = '[string](' + exprOfWord(l) + ')';
787
- const re = '[string](' + exprOfWord(r) + ')';
788
- if (op === '=' || op === '==')
1442
+ const FX_RE_FN = [
1443
+ 'function fx-re($a, $b) {',
1444
+ ' try {',
1445
+ ' $fx_rm = [regex]::Match([string]$a, (fx-posixre ([string]$b)), [Text.RegularExpressions.RegexOptions]::Singleline)',
1446
+ ' if ($fx_rm.Success) {',
1447
+ ' $fx_gs = @(); foreach ($fx_g in $fx_rm.Groups) { $fx_gs += ,[string]$fx_g.Value }',
1448
+ " fx-arrput 'BASH_REMATCH' $fx_gs",
1449
+ ' return $true',
1450
+ ' }',
1451
+ " fx-arrclr 'BASH_REMATCH'",
1452
+ ' return $false',
1453
+ ' }',
1454
+ " catch { [Console]::Error.WriteLine('bash: [[: invalid regular expression'); $script:fx_exit = 2; return $false }",
1455
+ '}',
1456
+ ].join('\n');
1457
+ const POSIX_CLASS_INNER = [
1458
+ [/\[:alnum:\]/g, '\\p{L}\\p{Nd}'],
1459
+ [/\[:alpha:\]/g, '\\p{L}'],
1460
+ [/\[:blank:\]/g, ' \\t'],
1461
+ [/\[:cntrl:\]/g, '\\x00-\\x1F\\x7F'],
1462
+ [/\[:digit:\]/g, '0-9'],
1463
+ [/\[:graph:\]/g, '\\x21-\\x7E'],
1464
+ [/\[:lower:\]/g, '\\p{Ll}'],
1465
+ [/\[:print:\]/g, '\\x20-\\x7E'],
1466
+ [/\[:punct:\]/g, '!-/:-@\\[-`{-~'],
1467
+ [/\[:space:\]/g, '\\s'],
1468
+ [/\[:upper:\]/g, '\\p{Lu}'],
1469
+ [/\[:word:\]/g, '\\p{L}\\p{Nd}_'],
1470
+ [/\[:xdigit:\]/g, '0-9A-Fa-f'],
1471
+ ];
1472
+ /** Replace `[:name:]` only inside an already-extracted bracket expression. */
1473
+ function replacePosixClassesInBracketInner(s) {
1474
+ let t = s;
1475
+ for (const [re, rep] of POSIX_CLASS_INNER)
1476
+ t = t.replace(re, rep);
1477
+ return t;
1478
+ }
1479
+ /** Index of the `]` that closes the `[` at `i`, skipping `[:name:]`. */
1480
+ function findBracketClose(s, i) {
1481
+ let j = i + 1;
1482
+ if (j < s.length && (s[j] === '!' || s[j] === '^'))
1483
+ j++;
1484
+ if (j < s.length && s[j] === ']')
1485
+ j++;
1486
+ while (j < s.length) {
1487
+ if (s.startsWith('[:', j)) {
1488
+ const end = s.indexOf(':]', j + 2);
1489
+ if (end >= 0) {
1490
+ j = end + 2;
1491
+ continue;
1492
+ }
1493
+ }
1494
+ if (s[j] === ']')
1495
+ return j;
1496
+ j++;
1497
+ }
1498
+ return -1;
1499
+ }
1500
+ /** POSIX ERE specials that keep their backslash; anything else is the char. */
1501
+ const ERE_KEEP_ESC = new Set([
1502
+ '.',
1503
+ '[',
1504
+ ']',
1505
+ '\\',
1506
+ '(',
1507
+ ')',
1508
+ '*',
1509
+ '+',
1510
+ '?',
1511
+ '{',
1512
+ '}',
1513
+ '|',
1514
+ '^',
1515
+ '$',
1516
+ '1',
1517
+ '2',
1518
+ '3',
1519
+ '4',
1520
+ '5',
1521
+ '6',
1522
+ '7',
1523
+ '8',
1524
+ '9',
1525
+ 'b',
1526
+ 'B',
1527
+ 'w',
1528
+ 'W',
1529
+ 's',
1530
+ 'S',
1531
+ ]);
1532
+ /**
1533
+ * Map a POSIX ERE to a .NET pattern: POSIX classes only inside `[…]`,
1534
+ * and drop .NET-only escapes (`\\d` → `d`) so `[[ 1 =~ $re ]]` with
1535
+ * `re='\\d'` stays false.
1536
+ */
1537
+ function rewriteEre(s) {
1538
+ let out = '';
1539
+ for (let i = 0; i < s.length; i++) {
1540
+ if (s[i] === '\\') {
1541
+ if (i + 1 >= s.length) {
1542
+ out += '\\\\';
1543
+ break;
1544
+ }
1545
+ const n = s[i + 1];
1546
+ out += ERE_KEEP_ESC.has(n) ? '\\' + n : n;
1547
+ i++;
1548
+ continue;
1549
+ }
1550
+ if (s[i] === '[') {
1551
+ const j = findBracketClose(s, i);
1552
+ if (j < 0) {
1553
+ out += '[';
1554
+ continue;
1555
+ }
1556
+ out += '[' + replacePosixClassesInBracketInner(s.slice(i + 1, j)) + ']';
1557
+ i = j;
1558
+ continue;
1559
+ }
1560
+ out += s[i];
1561
+ }
1562
+ return out;
1563
+ }
1564
+ /** Escape glob metacharacters so a quoted/escaped piece stays literal. */
1565
+ function globLitEsc(s) {
1566
+ return s.replace(/([*?[\]\\()\-])/g, '\\$1');
1567
+ }
1568
+ const FX_LIKEESC_FN = [
1569
+ 'function fx-likeesc($s) {',
1570
+ ' $fx_o = New-Object System.Text.StringBuilder',
1571
+ ' foreach ($fx_ch in ([string]$s).ToCharArray()) {',
1572
+ " if (@('*','?','[',']','`') -contains [string]$fx_ch) { [void]$fx_o.Append('`' + $fx_ch) }",
1573
+ ' else { [void]$fx_o.Append($fx_ch) }',
1574
+ ' }',
1575
+ ' $fx_o.ToString()',
1576
+ '}',
1577
+ ].join('\n');
1578
+ /** Build a regex pattern, escaping quoted/backslash-escaped portions. */
1579
+ function mergeUnescapedText(w) {
1580
+ const merged = [];
1581
+ for (const p of w) {
1582
+ const last = merged[merged.length - 1];
1583
+ if (p.kind === 'Text' &&
1584
+ !p.escaped &&
1585
+ last &&
1586
+ last.kind === 'Text' &&
1587
+ !last.escaped) {
1588
+ last.text += p.text;
1589
+ }
1590
+ else {
1591
+ merged.push(p.kind === 'Text' ? { ...p } : p);
1592
+ }
1593
+ }
1594
+ return merged;
1595
+ }
1596
+ function regexOperandExpr(w) {
1597
+ if (w.length === 0)
1598
+ return "''";
1599
+ const bits = [];
1600
+ let rest = w;
1601
+ if (w[0].kind === 'Text' && !w[0].escaped && w[0].text.startsWith('~')) {
1602
+ bits.push('[regex]::Escape([string](fx-home))');
1603
+ const after = w[0].text.slice(1);
1604
+ rest = after ? [{ kind: 'Text', text: after }, ...w.slice(1)] : w.slice(1);
1605
+ }
1606
+ for (const p of mergeUnescapedText(rest)) {
1607
+ if (p.kind === 'SingleQuoted') {
1608
+ bits.push('[regex]::Escape(' + psStr(p.text) + ')');
1609
+ }
1610
+ else if (p.kind === 'DoubleQuoted') {
1611
+ bits.push('[regex]::Escape([string](' + kshExprOfWord([p]) + '))');
1612
+ }
1613
+ else if (p.kind === 'Text') {
1614
+ bits.push(p.escaped
1615
+ ? '[regex]::Escape(' + psStr(p.text) + ')'
1616
+ : psStr(rewriteEre(p.text)));
1617
+ }
1618
+ else {
1619
+ bits.push('[string](' + kshExprOfWord([p]) + ')');
1620
+ }
1621
+ }
1622
+ return bits.length === 1 ? bits[0] : '(' + bits.join(' + ') + ')';
1623
+ }
1624
+ /** Build a bash glob string: quoted/escaped metas stay literal. */
1625
+ function globPatternExpr(w) {
1626
+ if (w.length === 0)
1627
+ return "''";
1628
+ const bits = [];
1629
+ let rest = w;
1630
+ if (w[0].kind === 'Text' && !w[0].escaped && w[0].text.startsWith('~')) {
1631
+ bits.push('(fx-gesc (([string](fx-home)).Replace([char]92, [char]47)))');
1632
+ const after = w[0].text.slice(1);
1633
+ rest = after ? [{ kind: 'Text', text: after }, ...w.slice(1)] : w.slice(1);
1634
+ }
1635
+ for (const p of mergeUnescapedText(rest)) {
1636
+ if (p.kind === 'SingleQuoted') {
1637
+ bits.push(psStr(globLitEsc(p.text)));
1638
+ }
1639
+ else if (p.kind === 'DoubleQuoted') {
1640
+ bits.push('(fx-gesc ([string](' + kshExprOfWord([p]) + ')))');
1641
+ }
1642
+ else if (p.kind === 'Text') {
1643
+ bits.push(psStr(p.escaped ? globLitEsc(p.text) : p.text));
1644
+ }
1645
+ else {
1646
+ bits.push('[string](' + kshExprOfWord([p]) + ')');
1647
+ }
1648
+ }
1649
+ return bits.length === 1 ? bits[0] : '(' + bits.join(' + ') + ')';
1650
+ }
1651
+ /** String operand for [[ compares — do not POSIX-normalize paths. */
1652
+ function stringOperandExpr(w) {
1653
+ if (w.length > 0 && w[0].kind === 'Text' && !w[0].escaped && w[0].text.startsWith('~')) {
1654
+ return '[string](' + kshExprOfWord(w) + ')';
1655
+ }
1656
+ const lit = w.every((p) => p.kind === 'Text' || p.kind === 'SingleQuoted')
1657
+ ? w.map((p) => p.text).join('')
1658
+ : null;
1659
+ if (lit !== null)
1660
+ return psStr(lit);
1661
+ return '[string](' + kshExprOfWord(w) + ')';
1662
+ }
1663
+ const FX_POSIX_INNER_PS = [
1664
+ " $inner = $inner.Replace('[:alnum:]', '\\p{L}\\p{Nd}')",
1665
+ " $inner = $inner.Replace('[:alpha:]', '\\p{L}')",
1666
+ " $inner = $inner.Replace('[:blank:]', ' \\t')",
1667
+ " $inner = $inner.Replace('[:cntrl:]', '\\x00-\\x1F\\x7F')",
1668
+ " $inner = $inner.Replace('[:digit:]', '0-9')",
1669
+ " $inner = $inner.Replace('[:graph:]', '\\x21-\\x7E')",
1670
+ " $inner = $inner.Replace('[:lower:]', '\\p{Ll}')",
1671
+ " $inner = $inner.Replace('[:print:]', '\\x20-\\x7E')",
1672
+ " $inner = $inner.Replace('[:punct:]', '!-/:-@\\[-`{-~')",
1673
+ " $inner = $inner.Replace('[:space:]', '\\s')",
1674
+ " $inner = $inner.Replace('[:upper:]', '\\p{Lu}')",
1675
+ " $inner = $inner.Replace('[:word:]', '\\p{L}\\p{Nd}_')",
1676
+ " $inner = $inner.Replace('[:xdigit:]', '0-9A-Fa-f')",
1677
+ ].join('\n');
1678
+ const FX_GMATCH_FN = [
1679
+ 'function fx-gesc($s) {',
1680
+ ' $o = New-Object System.Text.StringBuilder',
1681
+ ' foreach ($ch in ([string]$s).ToCharArray()) {',
1682
+ " if (@('*','?','[',']','\\','-','(',')') -contains [string]$ch) { [void]$o.Append('\\') }",
1683
+ ' [void]$o.Append($ch)',
1684
+ ' }',
1685
+ ' return $o.ToString()',
1686
+ '}',
1687
+ 'function fx-gposix([char]$ch, [string]$name) {',
1688
+ ' $c = [int]$ch',
1689
+ ' switch ($name) {',
1690
+ " 'digit' { return ($c -ge 48 -and $c -le 57) }",
1691
+ " 'xdigit' { return (($c -ge 48 -and $c -le 57) -or ($c -ge 65 -and $c -le 70) -or ($c -ge 97 -and $c -le 102)) }",
1692
+ " 'lower' { return [char]::IsLower($ch) }",
1693
+ " 'upper' { return [char]::IsUpper($ch) }",
1694
+ " 'alpha' { return [char]::IsLetter($ch) }",
1695
+ " 'alnum' { return ([char]::IsLetter($ch) -or [char]::IsDigit($ch)) }",
1696
+ " 'word' { return ([char]::IsLetter($ch) -or [char]::IsDigit($ch) -or $ch -eq [char]95) }",
1697
+ ' \'blank\' { return ($ch -eq [char]32 -or $ch -eq [char]9) }',
1698
+ ' \'space\' { return ($ch -eq [char]32 -or $ch -eq [char]9 -or $ch -eq [char]10 -or $ch -eq [char]11 -or $ch -eq [char]12 -or $ch -eq [char]13) }',
1699
+ ' \'cntrl\' { return ($c -le 31 -or $c -eq 127) }',
1700
+ " 'graph' { return ($c -ge 33 -and $c -le 126) }",
1701
+ " 'print' { return ($c -ge 32 -and $c -le 126) }",
1702
+ " 'punct' { return (($c -ge 33 -and $c -le 47) -or ($c -ge 58 -and $c -le 64) -or ($c -ge 91 -and $c -le 96) -or ($c -ge 123 -and $c -le 126)) }",
1703
+ ' default { return $false }',
1704
+ ' }',
1705
+ '}',
1706
+ 'function fx-ginclass([char]$ch, [string]$inner) {',
1707
+ ' $i = 0',
1708
+ ' while ($i -lt $inner.Length) {',
1709
+ ' if ($inner[$i] -eq [char]92 -and ($i + 1) -lt $inner.Length) {',
1710
+ ' if ([int]$inner[$i + 1] -ceq [int]$ch) { return $true }',
1711
+ ' $i += 2; continue',
1712
+ ' }',
1713
+ " if (($i + 1) -lt $inner.Length -and $inner[$i] -eq '[' -and $inner[$i + 1] -eq ':') {",
1714
+ " $k = $inner.IndexOf(':]', $i + 2)",
1715
+ ' if ($k -ge 0) {',
1716
+ ' if (fx-gposix $ch $inner.Substring($i + 2, $k - $i - 2)) { return $true }',
1717
+ ' $i = $k + 2; continue',
1718
+ ' }',
1719
+ ' }',
1720
+ ' if ($i -eq 0 -and $inner[$i] -eq [char]45) {',
1721
+ ' if ($ch -eq [char]45) { return $true }',
1722
+ ' $i++; continue',
1723
+ ' }',
1724
+ " if (($i + 2) -lt $inner.Length -and $inner[$i + 1] -eq '-' -and $inner[$i] -ne [char]92) {",
1725
+ ' $lo = [int][char]$inner[$i]; $hi = [int][char]$inner[$i + 2]',
1726
+ ' if ([int]$ch -ge $lo -and [int]$ch -le $hi) { return $true }',
1727
+ ' $i += 3; continue',
1728
+ ' }',
1729
+ ' if ([int]$inner[$i] -ceq [int]$ch) { return $true }',
1730
+ ' $i++',
1731
+ ' }',
1732
+ ' return $false',
1733
+ '}',
1734
+ 'function fx-gext([string]$p, [int]$pi) {',
1735
+ ' $pref = [string]$p[$pi]',
1736
+ ' $i = $pi + 2',
1737
+ ' $alts = New-Object System.Collections.ArrayList',
1738
+ ' $start = $i',
1739
+ ' $depth = 1',
1740
+ ' while ($i -lt $p.Length -and $depth -gt 0) {',
1741
+ ' $c = $p[$i]',
1742
+ ' if ($c -eq [char]92 -and ($i + 1) -lt $p.Length) { $i += 2; continue }',
1743
+ " if (($i + 1) -lt $p.Length -and $p[$i + 1] -eq '(' -and @('*','?','@','+','!') -contains [string]$c) { $depth++; $i += 2; continue }",
1744
+ " if ($c -eq ')') {",
1745
+ ' $depth--',
1746
+ ' if ($depth -eq 0) { [void]$alts.Add($p.Substring($start, $i - $start)); $i++; break }',
1747
+ ' $i++; continue',
1748
+ ' }',
1749
+ " if ($c -eq '|' -and $depth -eq 1) { [void]$alts.Add($p.Substring($start, $i - $start)); $i++; $start = $i; continue }",
1750
+ ' $i++',
1751
+ ' }',
1752
+ ' return @{ pref = $pref; alts = $alts; after = $i }',
1753
+ '}',
1754
+ 'function fx-gok([string]$s, [int]$si, [string]$p, [int]$pi) {',
1755
+ ' while ($pi -lt $p.Length) {',
1756
+ ' $c = $p[$pi]',
1757
+ ' if ($c -eq [char]92) {',
1758
+ ' if (($pi + 1) -ge $p.Length) {',
1759
+ ' if ($si -ge $s.Length -or $s[$si] -cne [char]92) { return $false }',
1760
+ ' $si++; $pi++; continue',
1761
+ ' }',
1762
+ ' $n = $p[$pi + 1]',
1763
+ ' if ($si -ge $s.Length -or $s[$si] -cne $n) { return $false }',
1764
+ ' $si++; $pi += 2; continue',
1765
+ ' }',
1766
+ " if (($pi + 1) -lt $p.Length -and $p[$pi + 1] -eq '(' -and @('*','?','@','+','!') -contains [string]$c) {",
1767
+ ' $ex = fx-gext $p $pi',
1768
+ ' $after = [int]$ex.after',
1769
+ ' $alts = @($ex.alts)',
1770
+ " if ($ex.pref -eq '!') {",
1771
+ ' for ($e = $si; $e -le $s.Length; $e++) {',
1772
+ ' $hit = $false',
1773
+ ' foreach ($alt in $alts) {',
1774
+ ' if (fx-gok $s.Substring($si, $e - $si) 0 ([string]$alt) 0) { $hit = $true; break }',
1775
+ ' }',
1776
+ ' if (-not $hit) { if (fx-gok $s $e $p $after) { return $true } }',
1777
+ ' }',
1778
+ ' return $false',
1779
+ ' }',
1780
+ " if ($ex.pref -eq '?' -or $ex.pref -eq '*') {",
1781
+ ' if (fx-gok $s $si $p $after) { return $true }',
1782
+ ' }',
1783
+ " if ($ex.pref -eq '@' -or $ex.pref -eq '?') {",
1784
+ ' foreach ($alt in $alts) {',
1785
+ ' for ($e = $si; $e -le $s.Length; $e++) {',
1786
+ ' if (fx-gok $s.Substring($si, $e - $si) 0 ([string]$alt) 0) {',
1787
+ ' if (fx-gok $s $e $p $after) { return $true }',
1788
+ ' }',
1789
+ ' }',
1790
+ ' }',
1791
+ ' return $false',
1792
+ ' }',
1793
+ ' $q = New-Object System.Collections.Generic.Queue[int]',
1794
+ " if ($ex.pref -eq '*') { $q.Enqueue($si) }",
1795
+ ' else {',
1796
+ ' foreach ($alt in $alts) {',
1797
+ ' for ($e = $si; $e -le $s.Length; $e++) {',
1798
+ ' if (fx-gok $s.Substring($si, $e - $si) 0 ([string]$alt) 0) { $q.Enqueue($e) }',
1799
+ ' }',
1800
+ ' }',
1801
+ ' }',
1802
+ ' $seen = New-Object System.Collections.Generic.HashSet[int]',
1803
+ ' while ($q.Count -gt 0) {',
1804
+ ' $pos = $q.Dequeue()',
1805
+ ' if (-not $seen.Add($pos)) { continue }',
1806
+ ' if (fx-gok $s $pos $p $after) { return $true }',
1807
+ ' foreach ($alt in $alts) {',
1808
+ ' for ($e = $pos + 1; $e -le $s.Length; $e++) {',
1809
+ ' if (fx-gok $s.Substring($pos, $e - $pos) 0 ([string]$alt) 0) { $q.Enqueue($e) }',
1810
+ ' }',
1811
+ ' }',
1812
+ ' }',
1813
+ ' return $false',
1814
+ ' }',
1815
+ " if ($c -eq '*') {",
1816
+ ' $pi++',
1817
+ ' if ($pi -ge $p.Length) { return $true }',
1818
+ ' for ($k = $si; $k -le $s.Length; $k++) { if (fx-gok $s $k $p $pi) { return $true } }',
1819
+ ' return $false',
1820
+ ' }',
1821
+ " if ($c -eq '?') {",
1822
+ ' if ($si -ge $s.Length) { return $false }',
1823
+ ' $si++; $pi++; continue',
1824
+ ' }',
1825
+ " if ($c -eq '[') {",
1826
+ ' $j = $pi + 1',
1827
+ " if ($j -lt $p.Length -and ($p[$j] -eq '!' -or $p[$j] -eq '^')) { $j++ }",
1828
+ " if ($j -lt $p.Length -and $p[$j] -eq ']') { $j++ }",
1829
+ ' while ($j -lt $p.Length) {',
1830
+ " if (($j + 1) -lt $p.Length -and $p[$j] -eq '[' -and $p[$j + 1] -eq ':') {",
1831
+ " $k = $p.IndexOf(':]', $j + 2)",
1832
+ ' if ($k -ge 0) { $j = $k + 2; continue }',
1833
+ ' }',
1834
+ " if ($p[$j] -eq ']') { break }",
1835
+ ' $j++',
1836
+ ' }',
1837
+ ' if ($j -ge $p.Length) {',
1838
+ " if ($si -ge $s.Length -or $s[$si] -cne '[') { return $false }",
1839
+ ' $si++; $pi++; continue',
1840
+ ' }',
1841
+ ' if ($si -ge $s.Length) { return $false }',
1842
+ ' $inner = $p.Substring($pi + 1, $j - $pi - 1)',
1843
+ ' $neg = $false',
1844
+ " if ($inner.StartsWith('!') -or $inner.StartsWith('^')) { $neg = $true; $inner = $inner.Substring(1) }",
1845
+ ' $ok = fx-ginclass $s[$si] $inner',
1846
+ ' if ($neg) { $ok = -not $ok }',
1847
+ ' if (-not $ok) { return $false }',
1848
+ ' $si++; $pi = $j + 1; continue',
1849
+ ' }',
1850
+ ' if ($si -ge $s.Length -or [int]$s[$si] -cne [int]$c) { return $false }',
1851
+ ' $si++; $pi++; continue',
1852
+ ' }',
1853
+ ' return ($si -eq $s.Length)',
1854
+ '}',
1855
+ 'function fx-gmatch($a, $b) {',
1856
+ ' $a = [string]$a; $b = [string]$b',
1857
+ ' if ($a.Length -ge 3 -and $a[1] -eq [char]58 -and $a[2] -eq [char]92) {',
1858
+ ' $a = $a.Replace([char]92, [char]47); $b = $b.Replace([char]92, [char]47)',
1859
+ ' } elseif ($b.Length -ge 3 -and $b[1] -eq [char]58 -and $b[2] -eq [char]92) {',
1860
+ ' $a = $a.Replace([char]92, [char]47); $b = $b.Replace([char]92, [char]47)',
1861
+ ' }',
1862
+ ' return [bool](fx-gok $a 0 $b 0)',
1863
+ '}',
1864
+ ].join('\n');
1865
+ const FX_POSIXRE_FN = [
1866
+ 'function fx-posixre($s) {',
1867
+ ' $t = [string]$s',
1868
+ ' $sb = New-Object System.Text.StringBuilder',
1869
+ ' $i = 0',
1870
+ " $keep = @('.','[',']','\\','(',')','*','+','?','{','}','|','^','$','1','2','3','4','5','6','7','8','9','b','B','w','W','s','S')",
1871
+ ' while ($i -lt $t.Length) {',
1872
+ ' $c = $t[$i]',
1873
+ " if ($c -eq '\\') {",
1874
+ " if (($i + 1) -ge $t.Length) { throw 'invalid regular expression' }",
1875
+ ' $n = $t[$i + 1]',
1876
+ " if ($keep -contains [string]$n) { [void]$sb.Append('\\' + [string]$n) }",
1877
+ ' else { [void]$sb.Append([string]$n) }',
1878
+ ' $i += 2',
1879
+ ' continue',
1880
+ ' }',
1881
+ " if ($c -eq '(' -and ($i + 1) -lt $t.Length -and $t[$i + 1] -eq '?') {",
1882
+ " throw 'invalid regular expression'",
1883
+ ' }',
1884
+ " if ($c -eq '[') {",
1885
+ ' $j = $i + 1',
1886
+ " if ($j -lt $t.Length -and ($t[$j] -eq '!' -or $t[$j] -eq '^')) { $j++ }",
1887
+ " if ($j -lt $t.Length -and $t[$j] -eq ']') { $j++ }",
1888
+ ' while ($j -lt $t.Length) {',
1889
+ " if (($j + 1) -lt $t.Length -and $t[$j] -eq '[' -and $t[$j + 1] -eq ':') {",
1890
+ " $k = $t.IndexOf(':]', $j + 2)",
1891
+ ' if ($k -ge 0) { $j = $k + 2; continue }',
1892
+ ' }',
1893
+ " if ($t[$j] -eq ']') { break }",
1894
+ ' $j++',
1895
+ ' }',
1896
+ " if ($j -ge $t.Length) { [void]$sb.Append('['); $i++; continue }",
1897
+ ' $inner = $t.Substring($i + 1, $j - $i - 1)',
1898
+ FX_POSIX_INNER_PS,
1899
+ " [void]$sb.Append('[' + $inner + ']')",
1900
+ ' $i = $j + 1',
1901
+ ' continue',
1902
+ ' }',
1903
+ ' if ($c -eq [char]36) { [void]$sb.Append(([string][char]92) + ([char]122)); $i++; continue }',
1904
+ " if ($c -eq '.') { [void]$sb.Append('(?:[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|.)'); $i++; continue }",
1905
+ ' [void]$sb.Append($c)',
1906
+ ' $i++',
1907
+ ' }',
1908
+ ' return $sb.ToString()',
1909
+ '}',
1910
+ ].join('\n');
1911
+ const FX_FCOMP_FN = [
1912
+ 'function fx-nt($a, $b) {',
1913
+ ' $a = [string]$a; $b = [string]$b',
1914
+ ' $ea = Test-Path -LiteralPath $a; $eb = Test-Path -LiteralPath $b',
1915
+ ' if ($ea -and -not $eb) { return $true }',
1916
+ ' if (-not $ea -or -not $eb) { return $false }',
1917
+ ' return ((Get-Item -LiteralPath $a -Force).LastWriteTimeUtc -gt (Get-Item -LiteralPath $b -Force).LastWriteTimeUtc)',
1918
+ '}',
1919
+ 'function fx-ot($a, $b) {',
1920
+ ' $a = [string]$a; $b = [string]$b',
1921
+ ' $ea = Test-Path -LiteralPath $a; $eb = Test-Path -LiteralPath $b',
1922
+ ' if ($eb -and -not $ea) { return $true }',
1923
+ ' if (-not $ea -or -not $eb) { return $false }',
1924
+ ' return ((Get-Item -LiteralPath $a -Force).LastWriteTimeUtc -lt (Get-Item -LiteralPath $b -Force).LastWriteTimeUtc)',
1925
+ '}',
1926
+ 'function fx-fid($p) {',
1927
+ ' $p = [string]$p',
1928
+ ' try { $id = ((& fsutil.exe file queryfileid $p 2>$null) | Out-String).Trim() } catch { return \'\' }',
1929
+ " if (-not $id) { return '' }",
1930
+ ' return ([IO.Path]::GetPathRoot($p) + \'|\' + $id)',
1931
+ '}',
1932
+ 'function fx-ef($a, $b) {',
1933
+ ' try {',
1934
+ ' $ia = Get-Item -LiteralPath ([string]$a) -Force -ErrorAction Stop',
1935
+ ' $ib = Get-Item -LiteralPath ([string]$b) -Force -ErrorAction Stop',
1936
+ ' } catch { return $false }',
1937
+ ' if ([string]$ia.FullName -ieq [string]$ib.FullName) { return $true }',
1938
+ ' $fa = fx-fid $ia.FullName; $fb = fx-fid $ib.FullName',
1939
+ " return ($fa -ne '' -and $fa -eq $fb)",
1940
+ '}',
1941
+ ].join('\n');
1942
+ function testBinaryExpr(l, op, r, allowKsh) {
1943
+ if (op === '-nt' || op === '-ot' || op === '-ef') {
1944
+ const lp = kshExprOfWord(l);
1945
+ const rp = kshExprOfWord(r);
1946
+ return '(fx-' + op.slice(1) + ' (' + lp + ') (' + rp + '))';
1947
+ }
1948
+ const le = allowKsh ? stringOperandExpr(l) : '[string](' + kshExprOfWord(l) + ')';
1949
+ const re = allowKsh ? stringOperandExpr(r) : '[string](' + kshExprOfWord(r) + ')';
1950
+ if (op === '=~')
1951
+ return '(fx-re (' + le + ') (' + regexOperandExpr(r) + '))';
1952
+ if (op === '>' || op === '<') {
1953
+ const cmp = '[string]::Compare(' + le + ', ' + re + ', [System.StringComparison]::Ordinal)';
1954
+ return '((' + cmp + ') ' + (op === '>' ? '-gt' : '-lt') + ' 0)';
1955
+ }
1956
+ if (op === '=' || op === '==') {
1957
+ if (allowKsh) {
1958
+ if (r.every((p) => p.kind === 'SingleQuoted' || p.kind === 'DoubleQuoted'))
1959
+ return '(' + le + ' -ceq ' + re + ')';
1960
+ return '(fx-gmatch (' + le + ') (' + globPatternExpr(r) + '))';
1961
+ }
789
1962
  return '(' + le + ' -ceq ' + re + ')';
790
- if (op === '!=')
1963
+ }
1964
+ if (op === '!=') {
1965
+ if (allowKsh) {
1966
+ if (r.every((p) => p.kind === 'SingleQuoted' || p.kind === 'DoubleQuoted'))
1967
+ return '(' + le + ' -cne ' + re + ')';
1968
+ return '(-not (fx-gmatch (' + le + ') (' + globPatternExpr(r) + ')))';
1969
+ }
791
1970
  return '(' + le + ' -cne ' + re + ')';
1971
+ }
1972
+ if (allowKsh)
1973
+ return '(fx-tnk (' + le + ') (' + re + ') ' + psStr(op) + ')';
792
1974
  return '(fx-tn (' + le + ') (' + re + ') ' + psStr(op) + ')';
793
1975
  }
794
- function parseTestOr(ws, st) {
795
- const r = parseTestAnd(ws, st);
1976
+ function parseTestOr(ws, st, allowRe) {
1977
+ const r = parseTestAnd(ws, st, allowRe);
796
1978
  if (r.error)
797
1979
  return r;
798
1980
  let expr = r.expr;
799
- while (st.i < ws.length && wordToString(ws[st.i]) === '-o') {
1981
+ while (st.i < ws.length &&
1982
+ (allowRe ? isUnquotedLiteral(ws[st.i], '||') : wordToString(ws[st.i]) === '-o')) {
800
1983
  st.i++;
801
- const rr = parseTestAnd(ws, st);
1984
+ const rr = parseTestAnd(ws, st, allowRe);
802
1985
  if (rr.error)
803
1986
  return rr;
804
1987
  expr = '(' + expr + ') -or (' + rr.expr + ')';
805
1988
  }
806
1989
  return { expr, error: null };
807
1990
  }
808
- function parseTestAnd(ws, st) {
809
- const r = parseTestNot(ws, st);
1991
+ function parseTestAnd(ws, st, allowRe) {
1992
+ const r = parseTestNot(ws, st, allowRe);
810
1993
  if (r.error)
811
1994
  return r;
812
1995
  let expr = r.expr;
813
- while (st.i < ws.length && wordToString(ws[st.i]) === '-a') {
1996
+ while (st.i < ws.length &&
1997
+ (allowRe ? isUnquotedLiteral(ws[st.i], '&&') : wordToString(ws[st.i]) === '-a')) {
814
1998
  st.i++;
815
- const rr = parseTestNot(ws, st);
1999
+ const rr = parseTestNot(ws, st, allowRe);
816
2000
  if (rr.error)
817
2001
  return rr;
818
2002
  expr = '(' + expr + ') -and (' + rr.expr + ')';
819
2003
  }
820
2004
  return { expr, error: null };
821
2005
  }
822
- function parseTestNot(ws, st) {
2006
+ function parseTestNot(ws, st, allowRe) {
823
2007
  const t = st.i < ws.length ? wordToString(ws[st.i]) : null;
824
- if (t === '!' && st.i + 1 < ws.length) {
2008
+ if (t === '!' && (!allowRe || isUnquotedLiteral(ws[st.i], '!'))) {
2009
+ if (st.i + 1 >= ws.length) {
2010
+ if (allowRe)
2011
+ return { expr: null, error: "`!': unary operator expected" };
2012
+ return parseTestAtom(ws, st, allowRe);
2013
+ }
825
2014
  st.i++;
826
- const r = parseTestNot(ws, st);
2015
+ const r = parseTestNot(ws, st, allowRe);
827
2016
  if (r.error)
828
2017
  return r;
829
2018
  return { expr: '(-not (' + r.expr + '))', error: null };
830
2019
  }
831
- return parseTestAtom(ws, st);
2020
+ return parseTestAtom(ws, st, allowRe);
832
2021
  }
833
- function parseTestAtom(ws, st) {
2022
+ function parseTestAtom(ws, st, allowRe) {
834
2023
  if (st.i >= ws.length)
835
2024
  return { expr: null, error: 'too many arguments' };
2025
+ if (allowRe && isUnquotedLiteral(ws[st.i], '(')) {
2026
+ st.i++;
2027
+ const inner = parseTestOr(ws, st, allowRe);
2028
+ if (inner.error)
2029
+ return inner;
2030
+ if (st.i >= ws.length || !isUnquotedLiteral(ws[st.i], ')')) {
2031
+ return { expr: null, error: "`)' expected" };
2032
+ }
2033
+ st.i++;
2034
+ return inner;
2035
+ }
836
2036
  const t = wordToString(ws[st.i]);
837
2037
  if (st.i === ws.length - 1) {
2038
+ if (allowRe && TEST_UNARY.has(t) && isUnquotedLiteral(ws[st.i], t)) {
2039
+ return { expr: null, error: t + ': unary operator expected' };
2040
+ }
2041
+ if (allowRe &&
2042
+ isUnquotedLiteral(ws[st.i], t) &&
2043
+ (t === '&&' || t === '||' || t === ')' || t === '(' || t === '<' || t === '>')) {
2044
+ return { expr: null, error: "syntax error near unexpected token `" + t + "'" };
2045
+ }
838
2046
  // single remaining word: bash test treats any non-null string as true
839
- const e = exprOfWord(ws[st.i]);
2047
+ const e = kshExprOfWord(ws[st.i]);
840
2048
  st.i++;
841
2049
  return { expr: strNe(e), error: null };
842
2050
  }
843
- if (TEST_UNARY.has(t)) {
2051
+ if (TEST_UNARY.has(t) && (!allowRe || isUnquotedLiteral(ws[st.i], t))) {
844
2052
  const w = ws[st.i + 1];
2053
+ if (allowRe &&
2054
+ (isUnquotedLiteral(w, '&&') ||
2055
+ isUnquotedLiteral(w, '||') ||
2056
+ isUnquotedLiteral(w, '<') ||
2057
+ isUnquotedLiteral(w, '>') ||
2058
+ isUnquotedLiteral(w, '|'))) {
2059
+ return { expr: null, error: t + ': unary operator expected' };
2060
+ }
845
2061
  st.i += 2;
846
2062
  return { expr: testUnaryExpr(t, w), error: null };
847
2063
  }
848
- const nt = wordToString(ws[st.i + 1]);
849
- if (TEST_BINARY.has(nt)) {
2064
+ const opWord = ws[st.i + 1];
2065
+ const nt = wordToString(opWord);
2066
+ const binaries = allowRe ? TEST_BINARY_KSH : TEST_BINARY;
2067
+ if ((!allowRe || isUnquotedLiteral(opWord, nt)) && binaries.has(nt)) {
850
2068
  if (st.i + 2 < ws.length) {
851
- const expr = testBinaryExpr(ws[st.i], nt, ws[st.i + 2]);
2069
+ const expr = testBinaryExpr(ws[st.i], nt, ws[st.i + 2], allowRe);
852
2070
  st.i += 3;
853
2071
  return { expr, error: null };
854
2072
  }
855
2073
  return { expr: null, error: 'OP:' + nt };
856
2074
  }
857
- const e = exprOfWord(ws[st.i]);
2075
+ const e = kshExprOfWord(ws[st.i]);
858
2076
  st.i++;
859
2077
  return { expr: strNe(e), error: null };
860
2078
  }
861
- function buildTest(ws, label) {
2079
+ function buildTest(ws, label, allowRe = false) {
862
2080
  if (ws.length === 0)
863
2081
  return '$script:fx_exit = 1';
864
2082
  const st = { i: 0 };
865
- const res = parseTestOr(ws, st);
2083
+ const res = parseTestOr(ws, st, allowRe);
866
2084
  let err = null;
867
2085
  if (res.error) {
868
2086
  err = res.error.startsWith('OP:')
@@ -873,14 +2091,53 @@ function buildTest(ws, label) {
873
2091
  err = 'bash: ' + label + ': too many arguments';
874
2092
  }
875
2093
  if (err !== null) {
2094
+ // [[ syntax errors must abort translation so later ; / && segments
2095
+ // cannot run (bash rejects the whole list).
2096
+ if (label === '[[')
2097
+ throw new FauxnixParseError(err);
876
2098
  return '[Console]::Error.WriteLine(' + psStr(err) + '); $script:fx_exit = 2';
877
2099
  }
2100
+ const helpers = [FX_TN_FN];
2101
+ if (allowRe && res.expr && res.expr.indexOf('fx-tnk') >= 0) {
2102
+ helpers.push(FX_TNK_FN);
2103
+ if (helpers.indexOf(FX_ENVGET_FN) < 0)
2104
+ helpers.push(FX_ENVGET_FN);
2105
+ }
2106
+ if (allowRe && res.expr && res.expr.indexOf('fx-re') >= 0) {
2107
+ helpers.push(FX_POSIXRE_FN, FX_RE_FN);
2108
+ }
2109
+ if (allowRe &&
2110
+ res.expr &&
2111
+ res.expr.indexOf('fx-posixre') >= 0 &&
2112
+ helpers.indexOf(FX_POSIXRE_FN) < 0)
2113
+ helpers.push(FX_POSIXRE_FN);
2114
+ if (allowRe &&
2115
+ res.expr &&
2116
+ (res.expr.indexOf('fx-gmatch') >= 0 || res.expr.indexOf('fx-gesc') >= 0))
2117
+ helpers.push(FX_GMATCH_FN);
2118
+ if (res.expr && res.expr.indexOf('fx-islink') >= 0)
2119
+ helpers.push(FX_ISLINK_FN);
2120
+ if (res.expr && res.expr.indexOf('fx-isset') >= 0)
2121
+ helpers.push(FX_ISSET_FN);
2122
+ if (allowRe && res.expr && res.expr.indexOf('fx-likeesc') >= 0)
2123
+ helpers.push(FX_LIKEESC_FN);
2124
+ if (res.expr &&
2125
+ (res.expr.indexOf('fx-envget') >= 0 || res.expr.indexOf('fx-home') >= 0))
2126
+ helpers.push(FX_ENVGET_FN);
2127
+ if (res.expr &&
2128
+ (res.expr.indexOf('fx-nt') >= 0 ||
2129
+ res.expr.indexOf('fx-ot') >= 0 ||
2130
+ res.expr.indexOf('fx-ef') >= 0))
2131
+ helpers.push(FX_FCOMP_FN);
878
2132
  return [
879
- FX_TN_FN,
2133
+ ...helpers,
880
2134
  '$fx_tr = ' + res.expr,
881
- 'if ($script:fx_exit -eq 2) { }',
882
- 'elseif (-not $fx_tr) { $script:fx_exit = 1 }',
883
- ].join('\n');
2135
+ allowRe
2136
+ ? 'if (-not $fx_tr) { if ($script:fx_exit -ne 2) { $script:fx_exit = 1 } } else { $script:fx_exit = 0 }'
2137
+ : 'if ($script:fx_exit -eq 2) { } elseif (-not $fx_tr) { $script:fx_exit = 1 }',
2138
+ ]
2139
+ .filter(Boolean)
2140
+ .join('\n');
884
2141
  }
885
2142
  const test = (args) => buildTest(args, 'test');
886
2143
  const bracket = (args) => {
@@ -889,6 +2146,12 @@ const bracket = (args) => {
889
2146
  }
890
2147
  return buildTest(args.slice(0, -1), '[');
891
2148
  };
2149
+ const dblBracket = (args) => {
2150
+ if (args.length === 0 || !isUnquotedLiteral(args[args.length - 1], ']]')) {
2151
+ return '[Console]::Error.WriteLine(' + psStr("bash: [[: missing `]]'") + '); $script:fx_exit = 2';
2152
+ }
2153
+ return buildTest(args.slice(0, -1), '[[', true);
2154
+ };
892
2155
  /* ------------------------------------------------------------------ */
893
2156
  /* pushd / popd / dirs */
894
2157
  /* ------------------------------------------------------------------ */
@@ -1119,6 +2382,7 @@ export const handlers = {
1119
2382
  false: falseCmd,
1120
2383
  test,
1121
2384
  '[': bracket,
2385
+ '[[': dblBracket,
1122
2386
  ':': colon,
1123
2387
  pushd,
1124
2388
  popd,