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