@optique/core 1.0.15-dev.2813 → 1.0.15-dev.2816

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.
@@ -1186,8 +1186,10 @@ def args-split []: string -> list<string> {
1186
1186
  mut current_token = ""
1187
1187
  mut result: list<string> = []
1188
1188
  mut prev_state = $STATE_NORMAL
1189
+ mut ends_with_delimiter = false
1189
1190
 
1190
1191
  for char in ($in | split chars) {
1192
+ $ends_with_delimiter = false
1191
1193
  if $state == $STATE_ESCAPE {
1192
1194
  $current_token = $current_token + $char
1193
1195
  $state = $prev_state
@@ -1200,6 +1202,7 @@ def args-split []: string -> list<string> {
1200
1202
  } else if $char == '"' {
1201
1203
  $state = $STATE_IN_DOUBLE_QUOTE
1202
1204
  } else if ($char in $WHITESPACES) {
1205
+ $ends_with_delimiter = true
1203
1206
  if ($current_token | is-not-empty) {
1204
1207
  $result = $result | append $current_token
1205
1208
  $current_token = ""
@@ -1221,12 +1224,52 @@ def args-split []: string -> list<string> {
1221
1224
  }
1222
1225
  }
1223
1226
  }
1224
- if ($current_token | is-not-empty) {
1227
+ # A quote left open at the end starts the argument being completed, even
1228
+ # when nothing follows it yet
1229
+ if ($current_token | is-not-empty) or ($state in [$STATE_IN_SINGLE_QUOTE $STATE_IN_DOUBLE_QUOTE]) {
1225
1230
  $result = $result | append $current_token
1226
1231
  }
1232
+ # Whitespace outside quotes at the end starts a new, still empty argument
1233
+ if $ends_with_delimiter {
1234
+ $result = $result | append ''
1235
+ }
1227
1236
  $result
1228
1237
  }
1229
1238
 
1239
+ # Helper to tell whether Nushell would not read a value back verbatim as a
1240
+ # single argument. A leading ! is included because the line editor expands
1241
+ # history (e.g., !!) there. With --allow-expansion, a leading tilde and n-dot
1242
+ # path segments (e.g., ...) are allowed to expand.
1243
+ def needs-quote [--allow-expansion]: string -> bool {
1244
+ let value = $in
1245
+ let special = ["'" '"' '\`' "#" "(" ")" "[" "]" "{" "}" "|" ";" "&" "$" "*" "?" "<" ">"]
1246
+ (
1247
+ ($value =~ '\\s')
1248
+ or ($special | any {|c| $value | str contains $c })
1249
+ or ($value =~ '^(\\+\\+|[-+*/])?=$')
1250
+ or ($value | str starts-with '!')
1251
+ or ((not $allow_expansion) and (
1252
+ ($value | str starts-with '~')
1253
+ or ($value | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
1254
+ ))
1255
+ )
1256
+ }
1257
+
1258
+ # Helper to quote a value so that Nushell reads it back verbatim as a
1259
+ # single argument. The line editor expands history at a ! that follows
1260
+ # whitespace even inside quotes, so such a ! is put between two quoted parts,
1261
+ # right after a quote, where it is left alone.
1262
+ def quote-arg []: string -> string {
1263
+ let value = $in
1264
+ if ($value | is-empty) or not ($value | needs-quote) {
1265
+ $value
1266
+ } else if ($value | str contains "'") {
1267
+ '"' + ($value | str replace -a '\\' '\\\\' | str replace -a '"' '\\"' | str replace -ar '(\\s)!' '\${1}"!"') + '"'
1268
+ } else {
1269
+ "'" + ($value | str replace -ar '(\\s)!' "\${1}'!'") + "'"
1270
+ }
1271
+ }
1272
+
1230
1273
  # Completion command that calls back to the CLI
1231
1274
  export def "${functionName}" [context: string] {
1232
1275
  # Split context into tokens, handling quotes properly
@@ -1239,18 +1282,11 @@ export def "${functionName}" [context: string] {
1239
1282
  []
1240
1283
  }
1241
1284
 
1242
- # If context ends with whitespace, add empty string to represent the new argument
1243
- let args_with_empty = if ($context | str ends-with ' ') {
1244
- $args | append ''
1245
- } else {
1246
- $args
1247
- }
1248
-
1249
1285
  # Ensure at least one argument (empty string) to get completions not script
1250
- let final_args = if ($args_with_empty | is-empty) {
1286
+ let final_args = if ($args | is-empty) {
1251
1287
  ['']
1252
1288
  } else {
1253
- $args_with_empty
1289
+ $args
1254
1290
  }
1255
1291
 
1256
1292
  let output = try {
@@ -1432,19 +1468,45 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1432
1468
  if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
1433
1469
  }
1434
1470
 
1435
- # Format file completions
1471
+ # Format file completions, quoting names that Nushell would not read
1472
+ # back as a single argument
1436
1473
  $filtered | each {|item|
1437
- let name = if $item.type == dir {
1438
- $dir_prefix + ($item.name | path basename) + "/"
1474
+ let base = if $item.type == dir {
1475
+ ($item.name | path basename) + "/"
1439
1476
  } else {
1440
- $dir_prefix + ($item.name | path basename)
1477
+ $item.name | path basename
1478
+ }
1479
+ let name = $dir_prefix + $base
1480
+ # Nushell expands a leading tilde even in a file name, but not after ./
1481
+ let spelled = if ($dir_prefix | is-empty) and ($base | str starts-with '~') {
1482
+ './' + $base
1483
+ } else {
1484
+ $name
1485
+ }
1486
+ # A tilde or n-dot prefix typed by the user is meant to be expanded
1487
+ # by Nushell, which quoting would prevent, so expand such a prefix
1488
+ # here when the value has to be quoted
1489
+ let literal_ndots = ($base | str replace -r '[/\\\\]$' '') =~ '^\\.{3,}$'
1490
+ let expandable = ($dir_prefix | str starts-with '~') or ($dir_prefix | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
1491
+ let value = if not ($literal_ndots or ($spelled | needs-quote --allow-expansion)) {
1492
+ $spelled
1493
+ } else if $expandable {
1494
+ let expanded_prefix = ($dir_prefix | path expand --no-symlink)
1495
+ let expanded_prefix = if ($expanded_prefix | str ends-with "/") { $expanded_prefix } else { $expanded_prefix + "/" }
1496
+ ($expanded_prefix + $base) | quote-arg
1497
+ } else {
1498
+ $spelled | quote-arg
1499
+ }
1500
+ if $value == $name {
1501
+ { value: $value }
1502
+ } else {
1503
+ { value: $value, display_override: $name }
1441
1504
  }
1442
- { value: $name }
1443
1505
  }
1444
1506
  } else {
1445
1507
  # Regular literal completion - split by tab
1446
1508
  let parts = ($line | split row "\t")
1447
- if ($parts | length) >= 2 {
1509
+ let record = if ($parts | length) >= 2 {
1448
1510
  # value\\tdescription format
1449
1511
  { value: ($parts | get 0), description: ($parts | get 1) }
1450
1512
  } else if ($parts | length) == 1 and ($parts | get 0 | is-not-empty) {
@@ -1453,6 +1515,18 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1453
1515
  } else {
1454
1516
  null
1455
1517
  }
1518
+ # Quote values that Nushell would not read back as a single argument,
1519
+ # while the menu keeps showing them as they are
1520
+ if $record == null {
1521
+ null
1522
+ } else {
1523
+ let value = ($record.value | quote-arg)
1524
+ if $value == $record.value {
1525
+ $record
1526
+ } else {
1527
+ $record | upsert value $value | insert display_override $record.value
1528
+ }
1529
+ }
1456
1530
  }
1457
1531
  } | flatten | compact
1458
1532
  }
@@ -1186,8 +1186,10 @@ def args-split []: string -> list<string> {
1186
1186
  mut current_token = ""
1187
1187
  mut result: list<string> = []
1188
1188
  mut prev_state = $STATE_NORMAL
1189
+ mut ends_with_delimiter = false
1189
1190
 
1190
1191
  for char in ($in | split chars) {
1192
+ $ends_with_delimiter = false
1191
1193
  if $state == $STATE_ESCAPE {
1192
1194
  $current_token = $current_token + $char
1193
1195
  $state = $prev_state
@@ -1200,6 +1202,7 @@ def args-split []: string -> list<string> {
1200
1202
  } else if $char == '"' {
1201
1203
  $state = $STATE_IN_DOUBLE_QUOTE
1202
1204
  } else if ($char in $WHITESPACES) {
1205
+ $ends_with_delimiter = true
1203
1206
  if ($current_token | is-not-empty) {
1204
1207
  $result = $result | append $current_token
1205
1208
  $current_token = ""
@@ -1221,12 +1224,52 @@ def args-split []: string -> list<string> {
1221
1224
  }
1222
1225
  }
1223
1226
  }
1224
- if ($current_token | is-not-empty) {
1227
+ # A quote left open at the end starts the argument being completed, even
1228
+ # when nothing follows it yet
1229
+ if ($current_token | is-not-empty) or ($state in [$STATE_IN_SINGLE_QUOTE $STATE_IN_DOUBLE_QUOTE]) {
1225
1230
  $result = $result | append $current_token
1226
1231
  }
1232
+ # Whitespace outside quotes at the end starts a new, still empty argument
1233
+ if $ends_with_delimiter {
1234
+ $result = $result | append ''
1235
+ }
1227
1236
  $result
1228
1237
  }
1229
1238
 
1239
+ # Helper to tell whether Nushell would not read a value back verbatim as a
1240
+ # single argument. A leading ! is included because the line editor expands
1241
+ # history (e.g., !!) there. With --allow-expansion, a leading tilde and n-dot
1242
+ # path segments (e.g., ...) are allowed to expand.
1243
+ def needs-quote [--allow-expansion]: string -> bool {
1244
+ let value = $in
1245
+ let special = ["'" '"' '\`' "#" "(" ")" "[" "]" "{" "}" "|" ";" "&" "$" "*" "?" "<" ">"]
1246
+ (
1247
+ ($value =~ '\\s')
1248
+ or ($special | any {|c| $value | str contains $c })
1249
+ or ($value =~ '^(\\+\\+|[-+*/])?=$')
1250
+ or ($value | str starts-with '!')
1251
+ or ((not $allow_expansion) and (
1252
+ ($value | str starts-with '~')
1253
+ or ($value | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
1254
+ ))
1255
+ )
1256
+ }
1257
+
1258
+ # Helper to quote a value so that Nushell reads it back verbatim as a
1259
+ # single argument. The line editor expands history at a ! that follows
1260
+ # whitespace even inside quotes, so such a ! is put between two quoted parts,
1261
+ # right after a quote, where it is left alone.
1262
+ def quote-arg []: string -> string {
1263
+ let value = $in
1264
+ if ($value | is-empty) or not ($value | needs-quote) {
1265
+ $value
1266
+ } else if ($value | str contains "'") {
1267
+ '"' + ($value | str replace -a '\\' '\\\\' | str replace -a '"' '\\"' | str replace -ar '(\\s)!' '\${1}"!"') + '"'
1268
+ } else {
1269
+ "'" + ($value | str replace -ar '(\\s)!' "\${1}'!'") + "'"
1270
+ }
1271
+ }
1272
+
1230
1273
  # Completion command that calls back to the CLI
1231
1274
  export def "${functionName}" [context: string] {
1232
1275
  # Split context into tokens, handling quotes properly
@@ -1239,18 +1282,11 @@ export def "${functionName}" [context: string] {
1239
1282
  []
1240
1283
  }
1241
1284
 
1242
- # If context ends with whitespace, add empty string to represent the new argument
1243
- let args_with_empty = if ($context | str ends-with ' ') {
1244
- $args | append ''
1245
- } else {
1246
- $args
1247
- }
1248
-
1249
1285
  # Ensure at least one argument (empty string) to get completions not script
1250
- let final_args = if ($args_with_empty | is-empty) {
1286
+ let final_args = if ($args | is-empty) {
1251
1287
  ['']
1252
1288
  } else {
1253
- $args_with_empty
1289
+ $args
1254
1290
  }
1255
1291
 
1256
1292
  let output = try {
@@ -1432,19 +1468,45 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1432
1468
  if ($parsed.parent | is-empty) { "" } else if ($parsed.parent | str ends-with "/") { $parsed.parent } else { $parsed.parent + "/" }
1433
1469
  }
1434
1470
 
1435
- # Format file completions
1471
+ # Format file completions, quoting names that Nushell would not read
1472
+ # back as a single argument
1436
1473
  $filtered | each {|item|
1437
- let name = if $item.type == dir {
1438
- $dir_prefix + ($item.name | path basename) + "/"
1474
+ let base = if $item.type == dir {
1475
+ ($item.name | path basename) + "/"
1439
1476
  } else {
1440
- $dir_prefix + ($item.name | path basename)
1477
+ $item.name | path basename
1478
+ }
1479
+ let name = $dir_prefix + $base
1480
+ # Nushell expands a leading tilde even in a file name, but not after ./
1481
+ let spelled = if ($dir_prefix | is-empty) and ($base | str starts-with '~') {
1482
+ './' + $base
1483
+ } else {
1484
+ $name
1485
+ }
1486
+ # A tilde or n-dot prefix typed by the user is meant to be expanded
1487
+ # by Nushell, which quoting would prevent, so expand such a prefix
1488
+ # here when the value has to be quoted
1489
+ let literal_ndots = ($base | str replace -r '[/\\\\]$' '') =~ '^\\.{3,}$'
1490
+ let expandable = ($dir_prefix | str starts-with '~') or ($dir_prefix | split row -r '[/\\\\]' | any {|s| $s =~ '^\\.{3,}$' })
1491
+ let value = if not ($literal_ndots or ($spelled | needs-quote --allow-expansion)) {
1492
+ $spelled
1493
+ } else if $expandable {
1494
+ let expanded_prefix = ($dir_prefix | path expand --no-symlink)
1495
+ let expanded_prefix = if ($expanded_prefix | str ends-with "/") { $expanded_prefix } else { $expanded_prefix + "/" }
1496
+ ($expanded_prefix + $base) | quote-arg
1497
+ } else {
1498
+ $spelled | quote-arg
1499
+ }
1500
+ if $value == $name {
1501
+ { value: $value }
1502
+ } else {
1503
+ { value: $value, display_override: $name }
1441
1504
  }
1442
- { value: $name }
1443
1505
  }
1444
1506
  } else {
1445
1507
  # Regular literal completion - split by tab
1446
1508
  let parts = ($line | split row "\t")
1447
- if ($parts | length) >= 2 {
1509
+ let record = if ($parts | length) >= 2 {
1448
1510
  # value\\tdescription format
1449
1511
  { value: ($parts | get 0), description: ($parts | get 1) }
1450
1512
  } else if ($parts | length) == 1 and ($parts | get 0 | is-not-empty) {
@@ -1453,6 +1515,18 @@ ${escapedArgs ? ` ^${programName} ${escapedArgs} ...$final_args | complete |
1453
1515
  } else {
1454
1516
  null
1455
1517
  }
1518
+ # Quote values that Nushell would not read back as a single argument,
1519
+ # while the menu keeps showing them as they are
1520
+ if $record == null {
1521
+ null
1522
+ } else {
1523
+ let value = ($record.value | quote-arg)
1524
+ if $value == $record.value {
1525
+ $record
1526
+ } else {
1527
+ $record | upsert value $value | insert display_override $record.value
1528
+ }
1529
+ }
1456
1530
  }
1457
1531
  } | flatten | compact
1458
1532
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.15-dev.2813",
3
+ "version": "1.0.15-dev.2816",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",