@optique/core 1.0.15-dev.2824 → 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)
@@ -1166,7 +1188,7 @@ complete -c ${programName} -f -a '(${functionName})'
1166
1188
  if (i > 0) yield "\n";
1167
1189
  if (suggestion.kind === "literal") {
1168
1190
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
1169
- yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
1191
+ yield `${encodeLiteral(suggestion.text)}\t${description}`;
1170
1192
  } else {
1171
1193
  const extensions = encodeExtensions(suggestion.extensions);
1172
1194
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1405,8 +1427,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1405
1427
  }
1406
1428
 
1407
1429
  # Process each line of output
1408
- $output | lines | each {|line|
1409
- 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__:') {
1410
1434
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1411
1435
  let directive = ($line | split row "\t" | first)
1412
1436
  let parts = ($directive | split row ':')
@@ -1466,22 +1490,26 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1466
1490
  # Generate file completions based on type.
1467
1491
  # When the glob base contains wildcard characters, use it as-is;
1468
1492
  # otherwise append * to treat it as a prefix.
1469
- # 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.
1470
1497
  let has_glob = ($glob_base =~ '[*?]')
1471
1498
  let ls_pattern = if ($glob_base | is-empty) {
1472
1499
  "."
1473
1500
  } else if $has_glob {
1474
1501
  ($glob_base | into glob)
1475
1502
  } else {
1476
- ($glob_base + "*" | into glob)
1503
+ (($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
1477
1504
  }
1478
1505
 
1479
1506
  # When using a glob pattern, also compute a directory listing pattern
1480
- # 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
1481
1509
  let glob_dir_pattern = if $has_glob {
1482
1510
  let dir_part = ($glob_base | path dirname)
1483
1511
  let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
1484
- ($dir + "/*" | into glob)
1512
+ (($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
1485
1513
  } else {
1486
1514
  null
1487
1515
  }
@@ -1674,7 +1702,7 @@ ${functionName}-external
1674
1702
  if (i > 0) yield "\n";
1675
1703
  if (suggestion.kind === "literal") {
1676
1704
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
1677
- yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
1705
+ yield `${encodeLiteral(suggestion.text)}\t${description}`;
1678
1706
  } else {
1679
1707
  const extensions = encodeExtensions(suggestion.extensions);
1680
1708
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1729,10 +1757,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1729
1757
 
1730
1758
  # Parse tab-separated output and create CompletionResult objects
1731
1759
  \$output -split "\`n" | ForEach-Object {
1760
+ \$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
1732
1761
  \$line = \$_.Trim()
1733
1762
  if (-not \$line) { return }
1763
+ if (\$literal) { \$line = \$line.Substring(12) }
1734
1764
 
1735
- if (\$line -match '^__FILE__:') {
1765
+ if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
1736
1766
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1737
1767
  \$directive = (\$line -split "\`t")[0]
1738
1768
  \$parts = \$directive -split ':', 5
@@ -1874,7 +1904,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1874
1904
  if (suggestion.kind === "literal") {
1875
1905
  const description = suggestion.description == null ? "" : sanitizeForTransport(require_message.formatMessage(suggestion.description, { colors: false }));
1876
1906
  const text = sanitizeForTransport(suggestion.text);
1877
- yield `${text}\t${text}\t${description}`;
1907
+ yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
1878
1908
  } else {
1879
1909
  const extensions = encodeExtensions(suggestion.extensions);
1880
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)
@@ -1166,7 +1188,7 @@ complete -c ${programName} -f -a '(${functionName})'
1166
1188
  if (i > 0) yield "\n";
1167
1189
  if (suggestion.kind === "literal") {
1168
1190
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
1169
- yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
1191
+ yield `${encodeLiteral(suggestion.text)}\t${description}`;
1170
1192
  } else {
1171
1193
  const extensions = encodeExtensions(suggestion.extensions);
1172
1194
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1405,8 +1427,10 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1405
1427
  }
1406
1428
 
1407
1429
  # Process each line of output
1408
- $output | lines | each {|line|
1409
- 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__:') {
1410
1434
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1411
1435
  let directive = ($line | split row "\t" | first)
1412
1436
  let parts = ($directive | split row ':')
@@ -1466,22 +1490,26 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1466
1490
  # Generate file completions based on type.
1467
1491
  # When the glob base contains wildcard characters, use it as-is;
1468
1492
  # otherwise append * to treat it as a prefix.
1469
- # 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.
1470
1497
  let has_glob = ($glob_base =~ '[*?]')
1471
1498
  let ls_pattern = if ($glob_base | is-empty) {
1472
1499
  "."
1473
1500
  } else if $has_glob {
1474
1501
  ($glob_base | into glob)
1475
1502
  } else {
1476
- ($glob_base + "*" | into glob)
1503
+ (($glob_base | str replace -a -r '([\\[\\]])' '[$1]') + "*" | into glob)
1477
1504
  }
1478
1505
 
1479
1506
  # When using a glob pattern, also compute a directory listing pattern
1480
- # 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
1481
1509
  let glob_dir_pattern = if $has_glob {
1482
1510
  let dir_part = ($glob_base | path dirname)
1483
1511
  let dir = if ($dir_part | is-empty) or ($dir_part == $glob_base) { "." } else { $dir_part }
1484
- ($dir + "/*" | into glob)
1512
+ (($dir | str replace -a -r '([\\[\\]])' '[$1]') + "/*" | into glob)
1485
1513
  } else {
1486
1514
  null
1487
1515
  }
@@ -1674,7 +1702,7 @@ ${functionName}-external
1674
1702
  if (i > 0) yield "\n";
1675
1703
  if (suggestion.kind === "literal") {
1676
1704
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
1677
- yield `${sanitizeForTransport(suggestion.text)}\t${description}`;
1705
+ yield `${encodeLiteral(suggestion.text)}\t${description}`;
1678
1706
  } else {
1679
1707
  const extensions = encodeExtensions(suggestion.extensions);
1680
1708
  const hidden = suggestion.includeHidden ? "1" : "0";
@@ -1729,10 +1757,12 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1729
1757
 
1730
1758
  # Parse tab-separated output and create CompletionResult objects
1731
1759
  \$output -split "\`n" | ForEach-Object {
1760
+ \$literal = \$_.StartsWith('__LITERAL__:', [System.StringComparison]::Ordinal)
1732
1761
  \$line = \$_.Trim()
1733
1762
  if (-not \$line) { return }
1763
+ if (\$literal) { \$line = \$line.Substring(12) }
1734
1764
 
1735
- if (\$line -match '^__FILE__:') {
1765
+ if (-not \$literal -and \$_.StartsWith('__FILE__:', [System.StringComparison]::Ordinal)) {
1736
1766
  # Parse file completion directive: __FILE__:type:extensions:pattern:hidden
1737
1767
  \$directive = (\$line -split "\`t")[0]
1738
1768
  \$parts = \$directive -split ':', 5
@@ -1874,7 +1904,7 @@ ${escapedArgs ? ` \$completionArgs += @(${escapedArgs})
1874
1904
  if (suggestion.kind === "literal") {
1875
1905
  const description = suggestion.description == null ? "" : sanitizeForTransport(formatMessage(suggestion.description, { colors: false }));
1876
1906
  const text = sanitizeForTransport(suggestion.text);
1877
- yield `${text}\t${text}\t${description}`;
1907
+ yield `${encodeLiteral(suggestion.text)}\t${text}\t${description}`;
1878
1908
  } else {
1879
1909
  const extensions = encodeExtensions(suggestion.extensions);
1880
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.2824",
3
+ "version": "1.0.15-dev.2835",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",