@optique/core 1.0.15-dev.2822 → 1.0.15-dev.2835

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.
@@ -62,6 +62,17 @@ function sanitizeForTransport(text) {
62
62
  return text.replace(/[\t\n\r\0]/g, " ");
63
63
  }
64
64
  /**
65
+ * Escapes literal values that would otherwise be interpreted as transport
66
+ * directives. Escape the escape prefix itself so decoding is unambiguous.
67
+ * @param text The literal suggestion text.
68
+ * @returns The sanitized and, if necessary, escaped literal text.
69
+ * @internal
70
+ */
71
+ function encodeLiteral(text) {
72
+ const sanitized = sanitizeForTransport(text);
73
+ return sanitized.startsWith("__FILE__:") || sanitized.startsWith("__LITERAL__:") ? `__LITERAL__:${sanitized}` : sanitized;
74
+ }
75
+ /**
65
76
  * The Bash shell completion generator.
66
77
  * @since 0.6.0
67
78
  */
@@ -308,10 +319,11 @@ function _${programName} () {
308
319
  __reply_prefix=""
309
320
  __file_current="$current"
310
321
  __literal=0
311
- if [[ "$line" == __LITERAL__:* ]]; then
322
+ # Protocol tags are case-sensitive even when nocasematch is enabled.
323
+ if [ "\${line:0:12}" = '__LITERAL__:' ]; then
312
324
  line="\${line#__LITERAL__:}"
313
325
  __literal=1
314
- elif [[ "$line" == __JOINED_FILE__:* ]]; then
326
+ elif [ "\${line:0:16}" = '__JOINED_FILE__:' ]; then
315
327
  line="\${line#__JOINED_FILE__:}"
316
328
  __reply_prefix="\${line%%:*}"
317
329
  __reply_prefix="\${__reply_prefix//%3A/:}"
@@ -332,7 +344,7 @@ function _${programName} () {
332
344
  __file_quote_offset=$((__file_quote_offset - \${#__reply_prefix}))
333
345
  fi
334
346
  fi
335
- if [[ $__literal == 0 && "$line" == __FILE__:* ]]; then
347
+ if [ "$__literal" = 0 ] && [ "\${line:0:9}" = '__FILE__:' ]; then
336
348
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
337
349
  IFS=':' read -r _ type extensions pattern hidden <<< "$line"
338
350
  extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
@@ -700,7 +712,12 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
700
712
  desc=\${line#*\$'\\0'}
701
713
  desc=\${desc%%\$'\\0'*}
702
714
 
703
- if [[ "\$value" == __FILE__:* ]]; then
715
+ local __literal=0
716
+ if [[ "\$value" == __LITERAL__:* ]]; then
717
+ value="\${value#__LITERAL__:}"
718
+ __literal=1
719
+ fi
720
+ if [[ \$__literal == 0 && "\$value" == __FILE__:* ]]; then
704
721
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
705
722
  local type extensions pattern hidden
706
723
  IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
@@ -816,7 +833,7 @@ fi
816
833
  *encodeSuggestions(suggestions) {
817
834
  for (const suggestion of suggestions) if (suggestion.kind === "literal") {
818
835
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
819
- yield `${sanitizeForTransport(suggestion.text)}\0${description}\0`;
836
+ yield `${encodeLiteral(suggestion.text)}\0${description}\0`;
820
837
  } else {
821
838
  const extensions = encodeExtensions(suggestion.extensions);
822
839
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -875,7 +892,12 @@ function ${functionName}
875
892
  ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
876
893
  # Process each line of output
877
894
  for line in $output
878
- if string match -q '__FILE__:*' -- $line
895
+ set -l literal 0
896
+ if string match -q '__LITERAL__:*' -- $line
897
+ set line (string sub -s 13 -- $line)
898
+ set literal 1
899
+ end
900
+ if test $literal -eq 0; and string match -q '__FILE__:*' -- $line
879
901
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
880
902
  set -l directive (string split \\t -- $line)[1]
881
903
  set -l parts (string split ':' -- $directive)
@@ -902,15 +924,13 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
902
924
  # Normalize leading ./ so that ./src/ma matches pattern src/
903
925
  set -l __norm_current (string replace -r '^\\./' '' -- "$current")
904
926
  set -l __norm_pattern (string replace -r '^\\./' '' -- "$pattern")
905
- # For wildcard patterns, compare only the directory prefix
927
+ # For wildcard patterns, compare only the directory prefix.
928
+ # Use a regex: fish wildcards have no character classes.
929
+ # Only the basename can hold wildcards; a ? or * in a
930
+ # directory name such as What?/ is literal.
906
931
  set -l __compare_pattern "$__norm_pattern"
907
- if string match -q '*[*?]*' -- "$__compare_pattern"
908
- set __compare_pattern (string replace -r '[*?].*' '' -- "$__compare_pattern")
909
- if string match -q '*/*' -- "$__compare_pattern"
910
- set __compare_pattern (string replace -r '/[^/]*$' '/' -- "$__compare_pattern")
911
- else
912
- set __compare_pattern ""
913
- end
932
+ if string match -qr '[*?][^/]*$' -- "$__compare_pattern"
933
+ set __compare_pattern (string replace -r '[^/]*$' '' -- "$__compare_pattern")
914
934
  end
915
935
  set -l __cp_len (string length -- "$__compare_pattern")
916
936
  set -l __nc_len (string length -- "$__norm_current")
@@ -945,54 +965,82 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
945
965
  end
946
966
 
947
967
  # Pre-expand file candidates. When the glob base came from
948
- # the program's pattern and contains * or ?, use it as the
949
- # complete glob expression. Otherwise append * as a prefix.
968
+ # the program's pattern and its basename contains * or ?, use
969
+ # it as the complete glob expression. Otherwise append * as a
970
+ # prefix, so a literal ? in a directory name such as What?/
971
+ # still lists that directory. A pattern equal to the current
972
+ # word is the typed text echoed back (as path() does), so its
973
+ # ? and * are literal characters, not wildcards.
950
974
  # Note: [ is NOT treated as a glob because it commonly
951
975
  # appears in literal filenames like [draft] or foo[1].txt.
952
976
  set -l __has_glob 0
953
977
  if test $__from_pattern -eq 1
954
- and string match -q '*[*?]*' -- "$glob_base"
978
+ and string match -qr '[*?][^/]*$' -- "$glob_base"
979
+ and test "$pattern" != "$current"
955
980
  set __has_glob 1
956
981
  end
957
982
  set -l __candidates
958
983
  if test $__has_glob -eq 1
959
984
  # Safe glob expansion without eval: split the pattern into
960
- # directory and filter parts, list the directory, then use
961
- # string match for wildcard filtering
962
- set -l __glob_dir (string replace -r '/[^/]*$' '' -- "$glob_base")
985
+ # a directory prefix and a basename filter, list the
986
+ # directory, then match basenames against a regex built
987
+ # from the filter. The prefix keeps one empty element for
988
+ # bare patterns so that $__glob_prefix* still expands.
989
+ set -l __glob_prefix ""
990
+ if string match -q '*/*' -- "$glob_base"
991
+ set __glob_prefix (string replace -r '[^/]*$' '' -- "$glob_base")
992
+ end
963
993
  set -l __glob_filter (string replace -r '.*/' '' -- "$glob_base")
964
- if test -z "$__glob_dir"; or test "$__glob_dir" = "$glob_base"
965
- set __glob_dir "."
994
+ # Translate the filter into an anchored regex: * and ? are
995
+ # wildcards and every other character is literal. Fish's
996
+ # own wildcards cannot be used here because ? is literal
997
+ # when the qmark-noglob feature is enabled (fish 4 default).
998
+ set -l __glob_regex '^'
999
+ for __ch in (string split '' -- "$__glob_filter")
1000
+ if test "$__ch" = '*'
1001
+ set __glob_regex "$__glob_regex.*"
1002
+ else if test "$__ch" = '?'
1003
+ set __glob_regex "$__glob_regex."
1004
+ else
1005
+ set __glob_regex "$__glob_regex"(string escape --style=regex -- "$__ch")
1006
+ end
966
1007
  end
1008
+ set __glob_regex "$__glob_regex"'$'
967
1009
  # Match files by the glob filter. Fish's * does not match
968
- # dotfiles, so also scan .* when the filter targets them.
969
- for __item in $__glob_dir/*
1010
+ # dotfiles, so also scan .* when the filter targets them or
1011
+ # hidden files are requested.
1012
+ set -l __entries $__glob_prefix*
1013
+ if test "$hidden" = "1"; or string match -q '.*' -- "$__glob_filter"
1014
+ set -a __entries $__glob_prefix.*
1015
+ end
1016
+ for __item in $__entries
970
1017
  if test -e "$__item"
971
- set -l __bn (basename -- "$__item")
972
- if string match -q -- "$__glob_filter" "$__bn"
973
- set -a __candidates "$__item"
1018
+ set -l __bn (string replace -r '.*/' '' -- "$__item")
1019
+ if contains -- "$__bn" . ..
1020
+ continue
974
1021
  end
975
- end
976
- end
977
- if string match -q '.*' -- "$__glob_filter"
978
- for __item in $__glob_dir/.*
979
- if test -e "$__item"
980
- set -l __bn (basename -- "$__item")
981
- if test "$__bn" = "." -o "$__bn" = ".."
982
- continue
983
- end
984
- if string match -q -- "$__glob_filter" "$__bn"
985
- set -a __candidates "$__item"
986
- end
1022
+ if string match -qr -- "$__glob_regex" "$__bn"
1023
+ set -a __candidates "$__item"
987
1024
  end
988
1025
  end
989
1026
  end
990
1027
  # For file/any modes, also include directories for navigation.
991
1028
  # Skip for directory mode so the pattern filter is respected.
1029
+ # Directories the filter already matched are not repeated.
992
1030
  if test "$type" != "directory"
993
- for __item in $__glob_dir/*/
1031
+ set -l __nav_dirs $__glob_prefix*/
1032
+ if test "$hidden" = "1"
1033
+ set -a __nav_dirs $__glob_prefix.*/
1034
+ end
1035
+ for __item in $__nav_dirs
1036
+ set __item (string replace -r '/$' '' -- "$__item")
1037
+ set -l __bn (string replace -r '.*/' '' -- "$__item")
1038
+ if contains -- "$__bn" . ..
1039
+ continue
1040
+ end
994
1041
  if test -d "$__item"
995
- set -a __candidates (string replace -r '/$' '' -- "$__item")
1042
+ and not string match -qr -- "$__glob_regex" "$__bn"
1043
+ set -a __candidates "$__item"
996
1044
  end
997
1045
  end
998
1046
  end
@@ -1140,7 +1188,7 @@ complete -c ${programName} -f -a '(${functionName})'
1140
1188
  if (i > 0) yield "\n";
1141
1189
  if (suggestion.kind === "literal") {
1142
1190
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
1143
- yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
1191
+ yield `${encodeLiteral(suggestion.text)}\t${description}`;
1144
1192
  } else {
1145
1193
  const extensions = encodeExtensions(suggestion.extensions);
1146
1194
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1379,8 +1427,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1379
1427
  }
1380
1428
 
1381
1429
  # Process each line of output
1382
- $output | lines | each {|line|
1383
- if ($line | str starts-with '__FILE__:') {
1430
+ $output | lines | each {|raw_line|
1431
+ let literal = ($raw_line | str starts-with '__LITERAL__:')
1432
+ let line = if $literal { $raw_line | str substring 12.. } else { $raw_line }
1433
+ if (not $literal) and ($line | str starts-with '__FILE__:') {
1384
1434
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1385
1435
  let directive = ($line | split row "\t" | first)
1386
1436
  let parts = ($directive | split row ':')
@@ -1440,22 +1490,26 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1440
1490
  # Generate file completions based on type.
1441
1491
  # When the glob base contains wildcard characters, use it as-is;
1442
1492
  # otherwise append * to treat it as a prefix.
1443
- # Note: into glob is required so that ls expands wildcards from a variable
1493
+ # Note: into glob is required so that ls expands wildcards from a variable.
1494
+ # A literal prefix has its brackets wrapped as [[] and []] so that path
1495
+ # text like "report[1]" is not parsed as a glob character class; a
1496
+ # wildcard pattern is a glob as a whole, so its brackets stay classes.
1444
1497
  let has_glob = ($glob_base =~ '[*?]')
1445
1498
  let ls_pattern = if ($glob_base | is-empty) {
1446
1499
  "."
1447
1500
  } else if $has_glob {
1448
1501
  ($glob_base | into glob)
1449
1502
  } else {
1450
- ($glob_base + "*" | into glob)
1503
+ (($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
1451
1504
  }
1452
1505
 
1453
1506
  # When using a glob pattern, also compute a directory listing pattern
1454
- # so that file/any modes can include directories for navigation
1507
+ # so that file/any modes can include directories for navigation; its
1508
+ # directory part is a literal path
1455
1509
  let glob_dir_pattern = if $has_glob {
1456
1510
  let dir_part = ($glob_base | path dirname)
1457
1511
  let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
1458
- ($dir + "/*" | into glob)
1512
+ (($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
1459
1513
  } else {
1460
1514
  null
1461
1515
  }
@@ -1648,7 +1702,7 @@ ${functionName}-external
1648
1702
  if (i > 0) yield "\n";
1649
1703
  if (suggestion.kind === "literal") {
1650
1704
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
1651
- yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
1705
+ yield `${encodeLiteral(suggestion.text)}\t${description}`;
1652
1706
  } else {
1653
1707
  const extensions = encodeExtensions(suggestion.extensions);
1654
1708
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1703,10 +1757,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1703
1757
 
1704
1758
  # Parse tab-separated output and create CompletionResult objects
1705
1759
  \$output -split "\`n" | ForEach-Object {
1760
+ \$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
1706
1761
  \$line = \$_.Trim()
1707
1762
  if (-not \$line) { return }
1763
+ if (\$literal) { \$line = \$line.Substring(12) }
1708
1764
 
1709
- if (\$line -match '^__FILE__:') {
1765
+ if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
1710
1766
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1711
1767
  \$directive = (\$line -split "\`t")[0]
1712
1768
  \$parts = \$directive -split ':', 5
@@ -1848,7 +1904,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1848
1904
  if (suggestion.kind === "literal") {
1849
1905
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
1850
1906
  const text = sanitizeForTransport(suggestion.text);
1851
- yield `${text}\t${text}\t${description}`;
1907
+ yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
1852
1908
  } else {
1853
1909
  const extensions = encodeExtensions(suggestion.extensions);
1854
1910
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -62,6 +62,17 @@ function sanitizeForTransport(text) {
62
62
  return text.replace(/[\t\n\r\0]/g, " ");
63
63
  }
64
64
  /**
65
+ * Escapes literal values that would otherwise be interpreted as transport
66
+ * directives. Escape the escape prefix itself so decoding is unambiguous.
67
+ * @param text The literal suggestion text.
68
+ * @returns The sanitized and, if necessary, escaped literal text.
69
+ * @internal
70
+ */
71
+ function encodeLiteral(text) {
72
+ const sanitized = sanitizeForTransport(text);
73
+ return sanitized.startsWith("__FILE__:") || sanitized.startsWith("__LITERAL__:") ? `__LITERAL__:${sanitized}` : sanitized;
74
+ }
75
+ /**
65
76
  * The Bash shell completion generator.
66
77
  * @since 0.6.0
67
78
  */
@@ -308,10 +319,11 @@ function _${programName} () {
308
319
  __reply_prefix=""
309
320
  __file_current="$current"
310
321
  __literal=0
311
- if [[ "$line" == __LITERAL__:* ]]; then
322
+ # Protocol tags are case-sensitive even when nocasematch is enabled.
323
+ if [ "\${line:0:12}" = '__LITERAL__:' ]; then
312
324
  line="\${line#__LITERAL__:}"
313
325
  __literal=1
314
- elif [[ "$line" == __JOINED_FILE__:* ]]; then
326
+ elif [ "\${line:0:16}" = '__JOINED_FILE__:' ]; then
315
327
  line="\${line#__JOINED_FILE__:}"
316
328
  __reply_prefix="\${line%%:*}"
317
329
  __reply_prefix="\${__reply_prefix//%3A/:}"
@@ -332,7 +344,7 @@ function _${programName} () {
332
344
  __file_quote_offset=$((__file_quote_offset - \${#__reply_prefix}))
333
345
  fi
334
346
  fi
335
- if [[ $__literal == 0 && "$line" == __FILE__:* ]]; then
347
+ if [ "$__literal" = 0 ] && [ "\${line:0:9}" = '__FILE__:' ]; then
336
348
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
337
349
  IFS=':' read -r _ type extensions pattern hidden <<< "$line"
338
350
  extensions="\${extensions//,\\./,}"; extensions="\${extensions#.}"
@@ -700,7 +712,12 @@ function _${programName.replace(/[^a-zA-Z0-9]/g, "_")} () {
700
712
  desc=\${line#*\$'\\0'}
701
713
  desc=\${desc%%\$'\\0'*}
702
714
 
703
- if [[ "\$value" == __FILE__:* ]]; then
715
+ local __literal=0
716
+ if [[ "\$value" == __LITERAL__:* ]]; then
717
+ value="\${value#__LITERAL__:}"
718
+ __literal=1
719
+ fi
720
+ if [[ \$__literal == 0 && "\$value" == __FILE__:* ]]; then
704
721
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
705
722
  local type extensions pattern hidden
706
723
  IFS=':' read -r _ type extensions pattern hidden <<< "\$value"
@@ -816,7 +833,7 @@ fi
816
833
  *encodeSuggestions(suggestions) {
817
834
  for (const suggestion of suggestions) if (suggestion.kind === "literal") {
818
835
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
819
- yield `${sanitizeForTransport(suggestion.text)}\0${description}\0`;
836
+ yield `${encodeLiteral(suggestion.text)}\0${description}\0`;
820
837
  } else {
821
838
  const extensions = encodeExtensions(suggestion.extensions);
822
839
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -875,7 +892,12 @@ function ${functionName}
875
892
  ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current 2>/dev/null)\n` : ` set -l output (${programName} $prev $current 2>/dev/null)\n`}
876
893
  # Process each line of output
877
894
  for line in $output
878
- if string match -q '__FILE__:*' -- $line
895
+ set -l literal 0
896
+ if string match -q '__LITERAL__:*' -- $line
897
+ set line (string sub -s 13 -- $line)
898
+ set literal 1
899
+ end
900
+ if test $literal -eq 0; and string match -q '__FILE__:*' -- $line
879
901
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
880
902
  set -l directive (string split \\t -- $line)[1]
881
903
  set -l parts (string split ':' -- $directive)
@@ -902,15 +924,13 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
902
924
  # Normalize leading ./ so that ./src/ma matches pattern src/
903
925
  set -l __norm_current (string replace -r '^\\./' '' -- "$current")
904
926
  set -l __norm_pattern (string replace -r '^\\./' '' -- "$pattern")
905
- # For wildcard patterns, compare only the directory prefix
927
+ # For wildcard patterns, compare only the directory prefix.
928
+ # Use a regex: fish wildcards have no character classes.
929
+ # Only the basename can hold wildcards; a ? or * in a
930
+ # directory name such as What?/ is literal.
906
931
  set -l __compare_pattern "$__norm_pattern"
907
- if string match -q '*[*?]*' -- "$__compare_pattern"
908
- set __compare_pattern (string replace -r '[*?].*' '' -- "$__compare_pattern")
909
- if string match -q '*/*' -- "$__compare_pattern"
910
- set __compare_pattern (string replace -r '/[^/]*$' '/' -- "$__compare_pattern")
911
- else
912
- set __compare_pattern ""
913
- end
932
+ if string match -qr '[*?][^/]*$' -- "$__compare_pattern"
933
+ set __compare_pattern (string replace -r '[^/]*$' '' -- "$__compare_pattern")
914
934
  end
915
935
  set -l __cp_len (string length -- "$__compare_pattern")
916
936
  set -l __nc_len (string length -- "$__norm_current")
@@ -945,54 +965,82 @@ ${escapedArgs ? ` set -l output (${programName} ${escapedArgs} $prev $current
945
965
  end
946
966
 
947
967
  # Pre-expand file candidates. When the glob base came from
948
- # the program's pattern and contains * or ?, use it as the
949
- # complete glob expression. Otherwise append * as a prefix.
968
+ # the program's pattern and its basename contains * or ?, use
969
+ # it as the complete glob expression. Otherwise append * as a
970
+ # prefix, so a literal ? in a directory name such as What?/
971
+ # still lists that directory. A pattern equal to the current
972
+ # word is the typed text echoed back (as path() does), so its
973
+ # ? and * are literal characters, not wildcards.
950
974
  # Note: [ is NOT treated as a glob because it commonly
951
975
  # appears in literal filenames like [draft] or foo[1].txt.
952
976
  set -l __has_glob 0
953
977
  if test $__from_pattern -eq 1
954
- and string match -q '*[*?]*' -- "$glob_base"
978
+ and string match -qr '[*?][^/]*$' -- "$glob_base"
979
+ and test "$pattern" != "$current"
955
980
  set __has_glob 1
956
981
  end
957
982
  set -l __candidates
958
983
  if test $__has_glob -eq 1
959
984
  # Safe glob expansion without eval: split the pattern into
960
- # directory and filter parts, list the directory, then use
961
- # string match for wildcard filtering
962
- set -l __glob_dir (string replace -r '/[^/]*$' '' -- "$glob_base")
985
+ # a directory prefix and a basename filter, list the
986
+ # directory, then match basenames against a regex built
987
+ # from the filter. The prefix keeps one empty element for
988
+ # bare patterns so that $__glob_prefix* still expands.
989
+ set -l __glob_prefix ""
990
+ if string match -q '*/*' -- "$glob_base"
991
+ set __glob_prefix (string replace -r '[^/]*$' '' -- "$glob_base")
992
+ end
963
993
  set -l __glob_filter (string replace -r '.*/' '' -- "$glob_base")
964
- if test -z "$__glob_dir"; or test "$__glob_dir" = "$glob_base"
965
- set __glob_dir "."
994
+ # Translate the filter into an anchored regex: * and ? are
995
+ # wildcards and every other character is literal. Fish's
996
+ # own wildcards cannot be used here because ? is literal
997
+ # when the qmark-noglob feature is enabled (fish 4 default).
998
+ set -l __glob_regex '^'
999
+ for __ch in (string split '' -- "$__glob_filter")
1000
+ if test "$__ch" = '*'
1001
+ set __glob_regex "$__glob_regex.*"
1002
+ else if test "$__ch" = '?'
1003
+ set __glob_regex "$__glob_regex."
1004
+ else
1005
+ set __glob_regex "$__glob_regex"(string escape --style=regex -- "$__ch")
1006
+ end
966
1007
  end
1008
+ set __glob_regex "$__glob_regex"'$'
967
1009
  # Match files by the glob filter. Fish's * does not match
968
- # dotfiles, so also scan .* when the filter targets them.
969
- for __item in $__glob_dir/*
1010
+ # dotfiles, so also scan .* when the filter targets them or
1011
+ # hidden files are requested.
1012
+ set -l __entries $__glob_prefix*
1013
+ if test "$hidden" = "1"; or string match -q '.*' -- "$__glob_filter"
1014
+ set -a __entries $__glob_prefix.*
1015
+ end
1016
+ for __item in $__entries
970
1017
  if test -e "$__item"
971
- set -l __bn (basename -- "$__item")
972
- if string match -q -- "$__glob_filter" "$__bn"
973
- set -a __candidates "$__item"
1018
+ set -l __bn (string replace -r '.*/' '' -- "$__item")
1019
+ if contains -- "$__bn" . ..
1020
+ continue
974
1021
  end
975
- end
976
- end
977
- if string match -q '.*' -- "$__glob_filter"
978
- for __item in $__glob_dir/.*
979
- if test -e "$__item"
980
- set -l __bn (basename -- "$__item")
981
- if test "$__bn" = "." -o "$__bn" = ".."
982
- continue
983
- end
984
- if string match -q -- "$__glob_filter" "$__bn"
985
- set -a __candidates "$__item"
986
- end
1022
+ if string match -qr -- "$__glob_regex" "$__bn"
1023
+ set -a __candidates "$__item"
987
1024
  end
988
1025
  end
989
1026
  end
990
1027
  # For file/any modes, also include directories for navigation.
991
1028
  # Skip for directory mode so the pattern filter is respected.
1029
+ # Directories the filter already matched are not repeated.
992
1030
  if test "$type" != "directory"
993
- for __item in $__glob_dir/*/
1031
+ set -l __nav_dirs $__glob_prefix*/
1032
+ if test "$hidden" = "1"
1033
+ set -a __nav_dirs $__glob_prefix.*/
1034
+ end
1035
+ for __item in $__nav_dirs
1036
+ set __item (string replace -r '/$' '' -- "$__item")
1037
+ set -l __bn (string replace -r '.*/' '' -- "$__item")
1038
+ if contains -- "$__bn" . ..
1039
+ continue
1040
+ end
994
1041
  if test -d "$__item"
995
- set -a __candidates (string replace -r '/$' '' -- "$__item")
1042
+ and not string match -qr -- "$__glob_regex" "$__bn"
1043
+ set -a __candidates "$__item"
996
1044
  end
997
1045
  end
998
1046
  end
@@ -1140,7 +1188,7 @@ complete -c ${programName} -f -a '(${functionName})'
1140
1188
  if (i > 0) yield "\n";
1141
1189
  if (suggestion.kind === "literal") {
1142
1190
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
1143
- yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
1191
+ yield `${encodeLiteral(suggestion.text)}\t${description}`;
1144
1192
  } else {
1145
1193
  const extensions = encodeExtensions(suggestion.extensions);
1146
1194
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1379,8 +1427,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1379
1427
  }
1380
1428
 
1381
1429
  # Process each line of output
1382
- $output | lines | each {|line|
1383
- if ($line | str starts-with '__FILE__:') {
1430
+ $output | lines | each {|raw_line|
1431
+ let literal = ($raw_line | str starts-with '__LITERAL__:')
1432
+ let line = if $literal { $raw_line | str substring 12.. } else { $raw_line }
1433
+ if (not $literal) and ($line | str starts-with '__FILE__:') {
1384
1434
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1385
1435
  let directive = ($line | split row "\t" | first)
1386
1436
  let parts = ($directive | split row ':')
@@ -1440,22 +1490,26 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1440
1490
  # Generate file completions based on type.
1441
1491
  # When the glob base contains wildcard characters, use it as-is;
1442
1492
  # otherwise append * to treat it as a prefix.
1443
- # Note: into glob is required so that ls expands wildcards from a variable
1493
+ # Note: into glob is required so that ls expands wildcards from a variable.
1494
+ # A literal prefix has its brackets wrapped as [[] and []] so that path
1495
+ # text like "report[1]" is not parsed as a glob character class; a
1496
+ # wildcard pattern is a glob as a whole, so its brackets stay classes.
1444
1497
  let has_glob = ($glob_base =~ '[*?]')
1445
1498
  let ls_pattern = if ($glob_base | is-empty) {
1446
1499
  "."
1447
1500
  } else if $has_glob {
1448
1501
  ($glob_base | into glob)
1449
1502
  } else {
1450
- ($glob_base + "*" | into glob)
1503
+ (($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
1451
1504
  }
1452
1505
 
1453
1506
  # When using a glob pattern, also compute a directory listing pattern
1454
- # so that file/any modes can include directories for navigation
1507
+ # so that file/any modes can include directories for navigation; its
1508
+ # directory part is a literal path
1455
1509
  let glob_dir_pattern = if $has_glob {
1456
1510
  let dir_part = ($glob_base | path dirname)
1457
1511
  let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
1458
- ($dir + "/*" | into glob)
1512
+ (($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
1459
1513
  } else {
1460
1514
  null
1461
1515
  }
@@ -1648,7 +1702,7 @@ ${functionName}-external
1648
1702
  if (i > 0) yield "\n";
1649
1703
  if (suggestion.kind === "literal") {
1650
1704
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
1651
- yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
1705
+ yield `${encodeLiteral(suggestion.text)}\t${description}`;
1652
1706
  } else {
1653
1707
  const extensions = encodeExtensions(suggestion.extensions);
1654
1708
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1703,10 +1757,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1703
1757
 
1704
1758
  # Parse tab-separated output and create CompletionResult objects
1705
1759
  \$output -split "\`n" | ForEach-Object {
1760
+ \$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
1706
1761
  \$line = \$_.Trim()
1707
1762
  if (-not \$line) { return }
1763
+ if (\$literal) { \$line = \$line.Substring(12) }
1708
1764
 
1709
- if (\$line -match '^__FILE__:') {
1765
+ if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
1710
1766
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1711
1767
  \$directive = (\$line -split "\`t")[0]
1712
1768
  \$parts = \$directive -split ':', 5
@@ -1848,7 +1904,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1848
1904
  if (suggestion.kind === "literal") {
1849
1905
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
1850
1906
  const text = sanitizeForTransport(suggestion.text);
1851
- yield `${text}\t${text}\t${description}`;
1907
+ yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
1852
1908
  } else {
1853
1909
  const extensions = encodeExtensions(suggestion.extensions);
1854
1910
  const hidden = suggestion.includeHidden ? "1" : "0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.15-dev.2822",
3
+ "version": "1.0.15-dev.2835",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",