From caaaa0f88765116b7be944ee70bdcb5076b93619 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:34:20 -0400 Subject: [PATCH 1/9] Remove extraneous back ticks in DocC in `Flag.swift`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/ArgumentParser/Parsable Properties/Flag.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index b9f940334..b350740ce 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -316,7 +316,7 @@ extension Flag where Value == Bool { /// ```swift /// @Flag(inversion: .prefixedNo) /// var useHTTPS: Bool = true - /// ```` + /// ``` /// /// - Parameters: /// - wrappedValue: A default value to use for this property, provided @@ -349,7 +349,7 @@ extension Flag where Value == Bool { /// ```swift /// @Flag(inversion: .prefixedNo) /// var useHTTPS: Bool - /// ```` + /// ``` /// /// - Parameters: /// - name: A specification for what names are allowed for this flag. From c95526e2327e0343d73b8bb98b58ca95a959d4f1 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 12 Sep 2025 08:47:36 -0400 Subject: [PATCH 2/9] Ignore spurious "Begin Documentation Comment With One Line Summary" swift-format violation. swift-format seems to have a bug. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/ArgumentParser/Parsable Properties/Flag.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index b350740ce..6c1106891 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -152,6 +152,7 @@ public struct FlagInversion: Hashable { self.init(base: .prefixedNo) } + // swift-format-ignore: BeginDocumentationCommentWithOneLineSummary /// Uses matching flags with `enable-` and `disable-` prefixes. /// /// For example, the `extraOutput` property in this declaration is set to From a2eadfbd45f42e5f208b523b7422ba8c944b773a Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:24:51 -0400 Subject: [PATCH 3/9] Complete repeatable positional instances after the first in bash. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 16 ++++++++++++---- .../testMathBashCompletionScript().bash | 12 ++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index c4e7c914d..3e50c09cb 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -217,7 +217,8 @@ extension CommandInfoV0 { result += """ \(declareTopLevelArray)flags=(\(flagCompletions.joined(separator: " "))) \(declareTopLevelArray)options=(\(optionCompletions.joined(separator: " "))) - \(offerFlagsOptionsFunctionName) \(positionalArguments.count) + \(offerFlagsOptionsFunctionName) \ + \(positionalArguments.contains { $0.isRepeating } ? 9_223_372_036_854_775_807 : positionalArguments.count) """ } @@ -248,14 +249,23 @@ extension CommandInfoV0 { """ } + var encounteredRepeatingPositional = false let positionalCases = zip(1..., positionalArguments) .compactMap { position, arg in + guard !encounteredRepeatingPositional else { + return nil as String? + } + + if arg.isRepeating { + encounteredRepeatingPositional = true + } + let completion = valueCompletion(arg) return completion.isEmpty ? nil : """ - \(position)) + \(encounteredRepeatingPositional ? "*" : position.description)) \(completion.indentingEachLine(by: 8))\ return ;; @@ -375,7 +385,6 @@ extension CommandInfoV0 { """ case .custom, .customAsync: - // Generate a call back into the command to retrieve a completions list return """ \(addCompletionsFunctionName) -W\ "$(\(customCompleteFunctionName) \(arg.commonCustomCompletionCall(command: self))\ @@ -385,7 +394,6 @@ extension CommandInfoV0 { """ case .customDeprecated: - // Generate a call back into the command to retrieve a completions list return """ \(addCompletionsFunctionName) -W\ "$(\(customCompleteFunctionName) \(arg.commonCustomCompletionCall(command: self)))" diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index dca1d8336..28b8bc07b 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -181,13 +181,13 @@ _math() { _math_add() { flags=(--hex-output -x --version -h --help) options=() - __math_offer_flags_options 1 + __math_offer_flags_options 9223372036854775807 } _math_multiply() { flags=(--hex-output -x --version -h --help) options=() - __math_offer_flags_options 1 + __math_offer_flags_options 9223372036854775807 } _math_stats() { @@ -214,7 +214,7 @@ _math_stats() { _math_stats_average() { flags=(--version -h --help) options=(--kind) - __math_offer_flags_options 1 + __math_offer_flags_options 9223372036854775807 # Offer option value completions case "${prev}" in @@ -228,13 +228,13 @@ _math_stats_average() { _math_stats_stdev() { flags=(--version -h --help) options=() - __math_offer_flags_options 1 + __math_offer_flags_options 9223372036854775807 } _math_stats_quantiles() { flags=(--version -h --help) options=(--file --directory --shell --custom --custom-deprecated) - __math_offer_flags_options 4 + __math_offer_flags_options 9223372036854775807 # Offer option value completions case "${prev}" in @@ -280,7 +280,7 @@ _math_stats_quantiles() { _math_help() { flags=(--version) options=() - __math_offer_flags_options 1 + __math_offer_flags_options 9223372036854775807 } complete -o filenames -F _math math From 71d51af9135eab8c76e82531b254901d3e45d355 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:22:56 -0400 Subject: [PATCH 4/9] Complete repeatable positional instances after the first in fish. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../FishCompletionsGenerator.swift | 43 +++++++++++++------ .../testMathFishCompletionScript().fish | 8 +++- .../Snapshots/testBase_Fish().fish | 8 +++- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index a984e3bdf..9773ea067 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -24,16 +24,20 @@ extension ToolInfoV0 { extension CommandInfoV0 { fileprivate var fishCompletionScript: String { """ - function \(shouldOfferCompletionsForFunctionName) -a expected_commands -a expected_positional_index + function \(shouldOfferCompletionsForFunctionName) -a expected_commands expected_positional_index positional_index_comparison set -l unparsed_tokens (\(tokensFunctionName) -pc) set -l positional_index 0 set -l commands + if test -z $positional_index_comparison + set positional_index_comparison -eq + end + switch $unparsed_tokens[1] \(commandCases) end - test "$commands" = "$expected_commands" -a \\( -z "$expected_positional_index" -o "$expected_positional_index" -eq "$positional_index" \\) + test "$commands" = "$expected_commands" -a \\( -z "$expected_positional_index" -o "$positional_index" "$positional_index_comparison" "$expected_positional_index" \\) end function \(tokensFunctionName) @@ -123,21 +127,32 @@ extension CommandInfoV0 { var positionalIndex = 0 + var repeatingPositionalComparison = "" let argumentCompletions = completableArguments - .map { arg in - """ - \(prefix)\( - arg.kind == .positional - ? """ - \({ - positionalIndex += 1 - return " \(positionalIndex)" - }()) + .compactMap { arg in + if arg.kind == .positional { + guard repeatingPositionalComparison.isEmpty else { + return nil as String? + } + + if arg.isRepeating { + repeatingPositionalComparison = " -ge" + } + } + + return """ + \(prefix)\( + arg.kind == .positional + ? """ + \({ + positionalIndex += 1 + return " \(positionalIndex)\(repeatingPositionalComparison)" + }()) + """ + : "" + )' \(argumentSegments(arg).joined(separator: separator)) """ - : "" - )' \(argumentSegments(arg).joined(separator: separator)) - """ } positionalIndex += 1 diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish index 269a72e54..819af5911 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish @@ -1,8 +1,12 @@ -function __math_should_offer_completions_for -a expected_commands -a expected_positional_index +function __math_should_offer_completions_for -a expected_commands expected_positional_index positional_index_comparison set -l unparsed_tokens (__math_tokens -pc) set -l positional_index 0 set -l commands + if test -z $positional_index_comparison + set positional_index_comparison -eq + end + switch $unparsed_tokens[1] case 'math' __math_parse_subcommand 0 'version' 'h/help' @@ -26,7 +30,7 @@ function __math_should_offer_completions_for -a expected_commands -a expected_po end end - test "$commands" = "$expected_commands" -a \( -z "$expected_positional_index" -o "$expected_positional_index" -eq "$positional_index" \) + test "$commands" = "$expected_commands" -a \( -z "$expected_positional_index" -o "$positional_index" "$positional_index_comparison" "$expected_positional_index" \) end function __math_tokens diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish index 4f58f7887..7c52c74bf 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish @@ -1,8 +1,12 @@ -function __base-test_should_offer_completions_for -a expected_commands -a expected_positional_index +function __base-test_should_offer_completions_for -a expected_commands expected_positional_index positional_index_comparison set -l unparsed_tokens (__base-test_tokens -pc) set -l positional_index 0 set -l commands + if test -z $positional_index_comparison + set positional_index_comparison -eq + end + switch $unparsed_tokens[1] case 'base-test' __base-test_parse_subcommand 2 'name=' 'kind=' 'other-kind=' 'path1=' 'path2=' 'path3=' 'one' 'two' 'custom-three' 'kind-counter' 'rep1=' 'r/rep2=' 'h/help' @@ -16,7 +20,7 @@ function __base-test_should_offer_completions_for -a expected_commands -a expect end end - test "$commands" = "$expected_commands" -a \( -z "$expected_positional_index" -o "$expected_positional_index" -eq "$positional_index" \) + test "$commands" = "$expected_commands" -a \( -z "$expected_positional_index" -o "$positional_index" "$positional_index_comparison" "$expected_positional_index" \) end function __base-test_tokens From ab21374201ba757d65a225dc7f105cc9fc9e61c7 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:18:31 -0400 Subject: [PATCH 5/9] Only complete positionals through the first repeating positional in zsh. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 87 ++++++++++--------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 2953d9399..210bb6e06 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -60,8 +60,52 @@ extension CommandInfoV0 { private var completionFunctions: String { let functionName = completionFunctionName - let argumentSpecsAndSetupScripts = (arguments ?? []).compactMap { - argumentSpecAndSetupScript($0) + var repeatingPositionalIndicator = "" + let argumentSpecsAndSetupScripts = (arguments ?? []).compactMap { arg in + guard arg.shouldDisplay else { + return nil as (argumentSpec: String, setupScript: String?)? + } + + let line: String + let names = arg.names ?? [] + switch names.count { + case 0: + guard repeatingPositionalIndicator.isEmpty else { + return nil + } + + if arg.isRepeating { + repeatingPositionalIndicator = "*" + } + line = repeatingPositionalIndicator + case 1: + // swift-format-ignore: NeverForceUnwrap + // Preconditions: names has exactly one element. + line = """ + \(arg.isRepeatingOption ? "*" : "")\(names.first!.commonCompletionSynopsisString().zshEscapeForSingleQuotedOptionSpec())\(arg.completionAbstract) + """ + default: + let synopses = names.map { + $0.commonCompletionSynopsisString() + .zshEscapeForSingleQuotedOptionSpec() + } + line = """ + \(arg.isRepeatingOption ? "*" : "(\(synopses.joined(separator: " ")))")'\ + {\(synopses.joined(separator: ","))}\ + '\(arg.completionAbstract) + """ + } + + switch arg.kind { + case .option, .positional: + let (argumentAction, setupScript) = argumentActionAndSetupScript(arg) + return ( + "'\(line):\(arg.valueName?.zshEscapeForSingleQuotedOptionSpec() ?? ""):\(argumentAction)'", + setupScript + ) + case .flag: + return ("'\(line)'", nil) + } } var argumentSpecs = argumentSpecsAndSetupScripts.map(\.argumentSpec) let setupScripts = argumentSpecsAndSetupScripts.compactMap(\.setupScript) @@ -139,45 +183,6 @@ extension CommandInfoV0 { """ } - private func argumentSpecAndSetupScript( - _ arg: ArgumentInfoV0 - ) -> (argumentSpec: String, setupScript: String?)? { - guard arg.shouldDisplay else { return nil } - - let line: String - let names = arg.names ?? [] - switch names.count { - case 0: - line = arg.isRepeating ? "*" : "" - case 1: - // swift-format-ignore: NeverForceUnwrap - // Preconditions: names has exactly one element. - line = """ - \(arg.isRepeatingOption ? "*" : "")\(names.first!.commonCompletionSynopsisString().zshEscapeForSingleQuotedOptionSpec())\(arg.completionAbstract) - """ - default: - let synopses = names.map { - $0.commonCompletionSynopsisString().zshEscapeForSingleQuotedOptionSpec() - } - line = """ - \(arg.isRepeatingOption ? "*" : "(\(synopses.joined(separator: " ")))")'\ - {\(synopses.joined(separator: ","))}\ - '\(arg.completionAbstract) - """ - } - - switch arg.kind { - case .option, .positional: - let (argumentAction, setupScript) = argumentActionAndSetupScript(arg) - return ( - "'\(line):\(arg.valueName?.zshEscapeForSingleQuotedOptionSpec() ?? ""):\(argumentAction)'", - setupScript - ) - case .flag: - return ("'\(line)'", nil) - } - } - /// Returns the zsh "action" for an argument completion string. private func argumentActionAndSetupScript( _ arg: ArgumentInfoV0 From a052808e882573f74ab69950792f403f4e0c6b00 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:41:26 -0400 Subject: [PATCH 6/9] Complete repeatable flag & option instances after the first in bash. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 74 +++++++---------- .../testMathBashCompletionScript().bash | 80 ++++++++++++------- .../Snapshots/testBase_Bash().bash | 50 +++++++----- 3 files changed, 107 insertions(+), 97 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 3e50c09cb..8e1fca2ed 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -48,15 +48,17 @@ extension CommandInfoV0 { # # required variables: # - # - flags: the flags that the current (sub)command can accept - # - options: the options that the current (sub)command can accept + # - repeating_flags: the repeating flags that the current (sub)command can accept + # - non_repeating_flags: the non-repeating flags that the current (sub)command can accept + # - repeating_options: the repeating options that the current (sub)command can accept + # - non_repeating_options: the non-repeating options that the current (sub)command can accept # - positional_number: value ignored # - unparsed_words: unparsed words from the current command line # # modified variables: # - # - flags: remove flags for this (sub)command that are already on the command line - # - options: remove options for this (sub)command that are already on the command line + # - non_repeating_flags: remove flags for this (sub)command that are already on the command line + # - non_repeating_options: remove options for this (sub)command that are already on the command line # - positional_number: set to the current positional number # - unparsed_words: remove all flags, options, and option values for this (sub)command \(offerFlagsOptionsFunctionName)() { @@ -93,26 +95,26 @@ extension CommandInfoV0 { # ${word} is a flag or an option # If ${word} is an option, mark that the next word to be parsed is an option value local option - for option in "${options[@]}"; do + for option in "${repeating_options[@]}" "${non_repeating_options[@]}"; do [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break done - # Remove ${word} from ${flags} or ${options} so it isn't offered again + # Remove ${word} from ${non_repeating_flags} or ${non_repeating_options} so it isn't offered again local not_found=true local -i index - for index in "${!flags[@]}"; do - if [[ "${flags[${index}]}" = "${word}" ]]; then - unset "flags[${index}]" - flags=("${flags[@]}") + for index in "${!non_repeating_flags[@]}"; do + if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then + unset "non_repeating_flags[${index}]" + non_repeating_flags=("${non_repeating_flags[@]}") not_found=false break fi done if "${not_found}"; then - for index in "${!options[@]}"; do - if [[ "${options[${index}]}" = "${word}" ]]; then - unset "options[${index}]" - options=("${options[@]}") + for index in "${!non_repeating_flags[@]}"; do + if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then + unset "non_repeating_flags[${index}]" + non_repeating_flags=("${non_repeating_flags[@]}") break fi done @@ -147,7 +149,7 @@ extension CommandInfoV0 { && ! "${is_parsing_option_value}"\\ && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] then - COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) + COMPREPLY+=($(compgen -W "${repeating_flags[*]} ${non_repeating_flags[*]} ${repeating_options[*]} ${non_repeating_options[*]}" -- "${cur}")) fi } @@ -211,12 +213,18 @@ extension CommandInfoV0 { let positionalArguments = positionalArguments - let flagCompletions = flagCompletions - let optionCompletions = optionCompletions - if !flagCompletions.isEmpty || !optionCompletions.isEmpty { + let arguments = arguments ?? [] + + let flags = arguments.filter { $0.kind == .flag } + let options = arguments.filter { $0.kind == .option } + if !flags.flatMap(\.completionWords).isEmpty + || !options.flatMap(\.completionWords).isEmpty + { result += """ - \(declareTopLevelArray)flags=(\(flagCompletions.joined(separator: " "))) - \(declareTopLevelArray)options=(\(optionCompletions.joined(separator: " "))) + \(declareTopLevelArray)repeating_flags=(\(flags.filter(\.isRepeating).flatMap(\.completionWords).joined(separator: " "))) + \(declareTopLevelArray)non_repeating_flags=(\(flags.filter { !$0.isRepeating }.flatMap(\.completionWords).joined(separator: " "))) + \(declareTopLevelArray)repeating_options=(\(options.filter(\.isRepeating).flatMap(\.completionWords).joined(separator: " "))) + \(declareTopLevelArray)non_repeating_options=(\(options.filter { !$0.isRepeating }.flatMap(\.completionWords).joined(separator: " "))) \(offerFlagsOptionsFunctionName) \ \(positionalArguments.contains { $0.isRepeating } ? 9_223_372_036_854_775_807 : positionalArguments.count) @@ -226,7 +234,7 @@ extension CommandInfoV0 { // Generate the case pattern-matching statements for option values. // If there aren't any, skip the case block altogether. let optionHandlers = - (arguments ?? []).compactMap { arg in + arguments.compactMap { arg in guard arg.kind != .flag else { return nil } let words = arg.completionWords guard !words.isEmpty else { return nil } @@ -320,30 +328,6 @@ extension CommandInfoV0 { """ } - /// Returns flag completions. - private var flagCompletions: [String] { - (arguments ?? []).flatMap { - switch $0.kind { - case .flag: - return $0.completionWords - default: - return [] - } - } - } - - /// Returns option completions. - private var optionCompletions: [String] { - (arguments ?? []).flatMap { - switch $0.kind { - case .option: - return $0.completionWords - default: - return [] - } - } - } - /// Returns the completions that can follow the given argument's `--name`. private func valueCompletion(_ arg: ArgumentInfoV0) -> String { switch arg.completionKind { diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 28b8bc07b..f8db8d4b7 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -22,15 +22,17 @@ __math_cursor_index_in_current_word() { # # required variables: # -# - flags: the flags that the current (sub)command can accept -# - options: the options that the current (sub)command can accept +# - repeating_flags: the repeating flags that the current (sub)command can accept +# - non_repeating_flags: the non-repeating flags that the current (sub)command can accept +# - repeating_options: the repeating options that the current (sub)command can accept +# - non_repeating_options: the non-repeating options that the current (sub)command can accept # - positional_number: value ignored # - unparsed_words: unparsed words from the current command line # # modified variables: # -# - flags: remove flags for this (sub)command that are already on the command line -# - options: remove options for this (sub)command that are already on the command line +# - non_repeating_flags: remove flags for this (sub)command that are already on the command line +# - non_repeating_options: remove options for this (sub)command that are already on the command line # - positional_number: set to the current positional number # - unparsed_words: remove all flags, options, and option values for this (sub)command __math_offer_flags_options() { @@ -67,26 +69,26 @@ __math_offer_flags_options() { # ${word} is a flag or an option # If ${word} is an option, mark that the next word to be parsed is an option value local option - for option in "${options[@]}"; do + for option in "${repeating_options[@]}" "${non_repeating_options[@]}"; do [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break done - # Remove ${word} from ${flags} or ${options} so it isn't offered again + # Remove ${word} from ${non_repeating_flags} or ${non_repeating_options} so it isn't offered again local not_found=true local -i index - for index in "${!flags[@]}"; do - if [[ "${flags[${index}]}" = "${word}" ]]; then - unset "flags[${index}]" - flags=("${flags[@]}") + for index in "${!non_repeating_flags[@]}"; do + if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then + unset "non_repeating_flags[${index}]" + non_repeating_flags=("${non_repeating_flags[@]}") not_found=false break fi done if "${not_found}"; then - for index in "${!options[@]}"; do - if [[ "${options[${index}]}" = "${word}" ]]; then - unset "options[${index}]" - options=("${options[@]}") + for index in "${!non_repeating_flags[@]}"; do + if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then + unset "non_repeating_flags[${index}]" + non_repeating_flags=("${non_repeating_flags[@]}") break fi done @@ -121,7 +123,7 @@ __math_offer_flags_options() { && ! "${is_parsing_option_value}"\ && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] then - COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) + COMPREPLY+=($(compgen -W "${repeating_flags[*]} ${non_repeating_flags[*]} ${repeating_options[*]} ${non_repeating_options[*]}" -- "${cur}")) fi } @@ -158,8 +160,10 @@ _math() { local -i positional_number local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}") - local -a flags=(--version -h --help) - local -a options=() + local -a repeating_flags=() + local -a non_repeating_flags=(--version -h --help) + local -a repeating_options=() + local -a non_repeating_options=() __math_offer_flags_options 0 # Offer subcommand / subcommand argument completions @@ -179,20 +183,26 @@ _math() { } _math_add() { - flags=(--hex-output -x --version -h --help) - options=() + repeating_flags=() + non_repeating_flags=(--hex-output -x --version -h --help) + repeating_options=() + non_repeating_options=() __math_offer_flags_options 9223372036854775807 } _math_multiply() { - flags=(--hex-output -x --version -h --help) - options=() + repeating_flags=() + non_repeating_flags=(--hex-output -x --version -h --help) + repeating_options=() + non_repeating_options=() __math_offer_flags_options 9223372036854775807 } _math_stats() { - flags=(--version -h --help) - options=() + repeating_flags=() + non_repeating_flags=(--version -h --help) + repeating_options=() + non_repeating_options=() __math_offer_flags_options 0 # Offer subcommand / subcommand argument completions @@ -212,8 +222,10 @@ _math_stats() { } _math_stats_average() { - flags=(--version -h --help) - options=(--kind) + repeating_flags=() + non_repeating_flags=(--version -h --help) + repeating_options=() + non_repeating_options=(--kind) __math_offer_flags_options 9223372036854775807 # Offer option value completions @@ -226,14 +238,18 @@ _math_stats_average() { } _math_stats_stdev() { - flags=(--version -h --help) - options=() + repeating_flags=() + non_repeating_flags=(--version -h --help) + repeating_options=() + non_repeating_options=() __math_offer_flags_options 9223372036854775807 } _math_stats_quantiles() { - flags=(--version -h --help) - options=(--file --directory --shell --custom --custom-deprecated) + repeating_flags=() + non_repeating_flags=(--version -h --help) + repeating_options=() + non_repeating_options=(--file --directory --shell --custom --custom-deprecated) __math_offer_flags_options 9223372036854775807 # Offer option value completions @@ -278,8 +294,10 @@ _math_stats_quantiles() { } _math_help() { - flags=(--version) - options=() + repeating_flags=() + non_repeating_flags=(--version) + repeating_options=() + non_repeating_options=() __math_offer_flags_options 9223372036854775807 } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 3c57c8601..762d10c1b 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -22,15 +22,17 @@ __base-test_cursor_index_in_current_word() { # # required variables: # -# - flags: the flags that the current (sub)command can accept -# - options: the options that the current (sub)command can accept +# - repeating_flags: the repeating flags that the current (sub)command can accept +# - non_repeating_flags: the non-repeating flags that the current (sub)command can accept +# - repeating_options: the repeating options that the current (sub)command can accept +# - non_repeating_options: the non-repeating options that the current (sub)command can accept # - positional_number: value ignored # - unparsed_words: unparsed words from the current command line # # modified variables: # -# - flags: remove flags for this (sub)command that are already on the command line -# - options: remove options for this (sub)command that are already on the command line +# - non_repeating_flags: remove flags for this (sub)command that are already on the command line +# - non_repeating_options: remove options for this (sub)command that are already on the command line # - positional_number: set to the current positional number # - unparsed_words: remove all flags, options, and option values for this (sub)command __base-test_offer_flags_options() { @@ -67,26 +69,26 @@ __base-test_offer_flags_options() { # ${word} is a flag or an option # If ${word} is an option, mark that the next word to be parsed is an option value local option - for option in "${options[@]}"; do + for option in "${repeating_options[@]}" "${non_repeating_options[@]}"; do [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break done - # Remove ${word} from ${flags} or ${options} so it isn't offered again + # Remove ${word} from ${non_repeating_flags} or ${non_repeating_options} so it isn't offered again local not_found=true local -i index - for index in "${!flags[@]}"; do - if [[ "${flags[${index}]}" = "${word}" ]]; then - unset "flags[${index}]" - flags=("${flags[@]}") + for index in "${!non_repeating_flags[@]}"; do + if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then + unset "non_repeating_flags[${index}]" + non_repeating_flags=("${non_repeating_flags[@]}") not_found=false break fi done if "${not_found}"; then - for index in "${!options[@]}"; do - if [[ "${options[${index}]}" = "${word}" ]]; then - unset "options[${index}]" - options=("${options[@]}") + for index in "${!non_repeating_flags[@]}"; do + if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then + unset "non_repeating_flags[${index}]" + non_repeating_flags=("${non_repeating_flags[@]}") break fi done @@ -121,7 +123,7 @@ __base-test_offer_flags_options() { && ! "${is_parsing_option_value}"\ && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] then - COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) + COMPREPLY+=($(compgen -W "${repeating_flags[*]} ${non_repeating_flags[*]} ${repeating_options[*]} ${non_repeating_options[*]}" -- "${cur}")) fi } @@ -158,8 +160,10 @@ _base-test() { local -i positional_number local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}") - local -a flags=(--one --two --custom-three --kind-counter -h --help) - local -a options=(--name --kind --other-kind --path1 --path2 --path3 --rep1 -r --rep2) + local -a repeating_flags=(--kind-counter) + local -a non_repeating_flags=(--one --two --custom-three -h --help) + local -a repeating_options=(--rep1 -r --rep2) + local -a non_repeating_options=(--name --kind --other-kind --path1 --path2 --path3) __base-test_offer_flags_options 2 # Offer option value completions @@ -224,14 +228,18 @@ _base-test() { } _base-test_sub-command() { - flags=(-h --help) - options=() + repeating_flags=() + non_repeating_flags=(-h --help) + repeating_options=() + non_repeating_options=() __base-test_offer_flags_options 0 } _base-test_escaped-command() { - flags=(-h --help) - options=(--o:n[e) + repeating_flags=() + non_repeating_flags=(-h --help) + repeating_options=() + non_repeating_options=(--o:n[e) __base-test_offer_flags_options 1 # Offer option value completions From f3e4dec0acd32a24b9bee404f9c497311eaf9d54 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 12 Sep 2025 18:26:24 -0400 Subject: [PATCH 7/9] Complete non-repeatable flag & option instances only once in fish. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../FishCompletionsGenerator.swift | 71 ++++++++---- .../testMathFishCompletionScript().fish | 103 +++++++++++------- .../Snapshots/testBase_Fish().fish | 83 +++++++++----- 3 files changed, 168 insertions(+), 89 deletions(-) diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index 9773ea067..11d90e568 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -24,20 +24,36 @@ extension ToolInfoV0 { extension CommandInfoV0 { fileprivate var fishCompletionScript: String { """ - function \(shouldOfferCompletionsForFunctionName) -a expected_commands expected_positional_index positional_index_comparison - set -l unparsed_tokens (\(tokensFunctionName) -pc) + function \(shouldOfferCompletionsForFlagsOrOptionsFunctionName) -a expected_commands + set -l non_repeating_flags_or_options $argv[2..] + + set -l non_repeating_flags_or_options_absent 0 set -l positional_index 0 set -l commands + \(parseTokensFunctionName) + test "$commands" = "$expected_commands"; and return $non_repeating_flags_or_options_absent + end + function \(shouldOfferCompletionsForPositionalFunctionName) -a expected_commands expected_positional_index positional_index_comparison if test -z $positional_index_comparison set positional_index_comparison -eq end + set -l non_repeating_flags_or_options + set -l non_repeating_flags_or_options_absent 0 + set -l positional_index 0 + set -l commands + \(parseTokensFunctionName) + test "$commands" = "$expected_commands" -a \\( "$positional_index" "$positional_index_comparison" "$expected_positional_index" \\) + end + + function \(parseTokensFunctionName) -S + set -l unparsed_tokens (\(tokensFunctionName) -pc) + set -l present_flags_and_options + switch $unparsed_tokens[1] \(commandCases) end - - test "$commands" = "$expected_commands" -a \\( -z "$expected_positional_index" -o "$positional_index" "$positional_index_comparison" "$expected_positional_index" \\) end function \(tokensFunctionName) @@ -48,9 +64,8 @@ extension CommandInfoV0 { end end - function \(parseSubcommandFunctionName) -S + function \(parseSubcommandFunctionName) -S -a positional_count argparse -s r -- $argv - set -l positional_count $argv[1] set -l option_specs $argv[2..] set -a commands $unparsed_tokens[1] @@ -62,8 +77,16 @@ extension CommandInfoV0 { argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null set unparsed_tokens $argv set positional_index (math $positional_index + 1) + + for non_repeating_flag_or_option in $non_repeating_flags_or_options + if set -ql _flag_$non_repeating_flag_or_option + set non_repeating_flags_or_options_absent 1 + break + end + end + if test (count $unparsed_tokens) -eq 0 -o \\( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \\) - return 0 + break end set -e unparsed_tokens[1] end @@ -117,11 +140,7 @@ extension CommandInfoV0 { } private var completions: [String] { - let prefix = """ - complete -c '\(initialCommand)'\ - -n '\(shouldOfferCompletionsForFunctionName)\ - "\(commandContext.joined(separator: separator))" - """ + let prefix = "complete -c '\(initialCommand)' -n '" let subcommands = (subcommands ?? []).filter(\.shouldDisplay) @@ -145,12 +164,15 @@ extension CommandInfoV0 { \(prefix)\( arg.kind == .positional ? """ - \({ + \(shouldOfferCompletionsForPositionalFunctionName) "\(commandContext.joined(separator: separator))" \({ positionalIndex += 1 - return " \(positionalIndex)\(repeatingPositionalComparison)" + return "\(positionalIndex)\(repeatingPositionalComparison)" }()) """ - : "" + : """ + \(shouldOfferCompletionsForFlagsOrOptionsFunctionName) "\(commandContext.joined(separator: separator))"\ + \((arg.isRepeating ? [] : arg.names ?? []).map { " \($0.name)" }.sorted().joined()) + """ )' \(argumentSegments(arg).joined(separator: separator)) """ } @@ -160,7 +182,10 @@ extension CommandInfoV0 { return argumentCompletions + subcommands.map { - "\(prefix) \(positionalIndex)' -fa '\($0.commandName)' -d '\($0.abstract?.fishEscapeForSingleQuotedString() ?? "")'" + """ + \(prefix)\(shouldOfferCompletionsForPositionalFunctionName) "\(commandContext.joined(separator: separator))"\ + \(positionalIndex)' -fa '\($0.commandName)' -d '\($0.abstract?.fishEscapeForSingleQuotedString() ?? "")' + """ } + subcommands.flatMap(\.completions) } @@ -262,8 +287,16 @@ extension CommandInfoV0 { """ } - private var shouldOfferCompletionsForFunctionName: String { - "\(completionFunctionPrefix)_should_offer_completions_for" + private var shouldOfferCompletionsForFlagsOrOptionsFunctionName: String { + "\(completionFunctionPrefix)_should_offer_completions_for_flags_or_options" + } + + private var shouldOfferCompletionsForPositionalFunctionName: String { + "\(completionFunctionPrefix)_should_offer_completions_for_positional" + } + + private var parseTokensFunctionName: String { + "\(completionFunctionPrefix)_parse_tokens" } private var tokensFunctionName: String { @@ -304,7 +337,7 @@ extension ArgumentInfoV0 { private func optionSpecRequiresValue(_ optionSpec: String) -> String { switch kind { case .option: - return "\(optionSpec)=" + return "\(optionSpec)=\(isRepeating ? "+" : "")" default: return optionSpec } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish index 819af5911..0fd19c10d 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathFishCompletionScript().fish @@ -1,12 +1,30 @@ -function __math_should_offer_completions_for -a expected_commands expected_positional_index positional_index_comparison - set -l unparsed_tokens (__math_tokens -pc) +function __math_should_offer_completions_for_flags_or_options -a expected_commands + set -l non_repeating_flags_or_options $argv[2..] + + set -l non_repeating_flags_or_options_absent 0 set -l positional_index 0 set -l commands + __math_parse_tokens + test "$commands" = "$expected_commands"; and return $non_repeating_flags_or_options_absent +end +function __math_should_offer_completions_for_positional -a expected_commands expected_positional_index positional_index_comparison if test -z $positional_index_comparison set positional_index_comparison -eq end + set -l non_repeating_flags_or_options + set -l non_repeating_flags_or_options_absent 0 + set -l positional_index 0 + set -l commands + __math_parse_tokens + test "$commands" = "$expected_commands" -a \( "$positional_index" "$positional_index_comparison" "$expected_positional_index" \) +end + +function __math_parse_tokens -S + set -l unparsed_tokens (__math_tokens -pc) + set -l present_flags_and_options + switch $unparsed_tokens[1] case 'math' __math_parse_subcommand 0 'version' 'h/help' @@ -29,8 +47,6 @@ function __math_should_offer_completions_for -a expected_commands expected_posit __math_parse_subcommand -r 1 'version' end end - - test "$commands" = "$expected_commands" -a \( -z "$expected_positional_index" -o "$positional_index" "$positional_index_comparison" "$expected_positional_index" \) end function __math_tokens @@ -41,9 +57,8 @@ function __math_tokens end end -function __math_parse_subcommand -S +function __math_parse_subcommand -S -a positional_count argparse -s r -- $argv - set -l positional_count $argv[1] set -l option_specs $argv[2..] set -a commands $unparsed_tokens[1] @@ -55,8 +70,16 @@ function __math_parse_subcommand -S argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null set unparsed_tokens $argv set positional_index (math $positional_index + 1) + + for non_repeating_flag_or_option in $non_repeating_flags_or_options + if set -ql _flag_$non_repeating_flag_or_option + set non_repeating_flags_or_options_absent 1 + break + end + end + if test (count $unparsed_tokens) -eq 0 -o \( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \) - return 0 + break end set -e unparsed_tokens[1] end @@ -82,36 +105,36 @@ function __math_custom_completion end complete -c 'math' -f -complete -c 'math' -n '__math_should_offer_completions_for "math"' -l 'version' -d 'Show the version.' -complete -c 'math' -n '__math_should_offer_completions_for "math"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'math' -n '__math_should_offer_completions_for "math" 1' -fa 'add' -d 'Print the sum of the values.' -complete -c 'math' -n '__math_should_offer_completions_for "math" 1' -fa 'multiply' -d 'Print the product of the values.' -complete -c 'math' -n '__math_should_offer_completions_for "math" 1' -fa 'stats' -d 'Calculate descriptive statistics.' -complete -c 'math' -n '__math_should_offer_completions_for "math" 1' -fa 'help' -d 'Show subcommand help information.' -complete -c 'math' -n '__math_should_offer_completions_for "math add"' -l 'hex-output' -s 'x' -d 'Use hexadecimal notation for the result.' -complete -c 'math' -n '__math_should_offer_completions_for "math add"' -l 'version' -d 'Show the version.' -complete -c 'math' -n '__math_should_offer_completions_for "math add"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'math' -n '__math_should_offer_completions_for "math multiply"' -l 'hex-output' -s 'x' -d 'Use hexadecimal notation for the result.' -complete -c 'math' -n '__math_should_offer_completions_for "math multiply"' -l 'version' -d 'Show the version.' -complete -c 'math' -n '__math_should_offer_completions_for "math multiply"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats"' -l 'version' -d 'Show the version.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats" 1' -fa 'average' -d 'Print the average of the values.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats" 1' -fa 'stdev' -d 'Print the standard deviation of the values.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats" 1' -fa 'quantiles' -d 'Print the quantiles of the values (TBD).' -complete -c 'math' -n '__math_should_offer_completions_for "math stats average"' -l 'kind' -d 'The kind of average to provide.' -rfka 'mean median mode' -complete -c 'math' -n '__math_should_offer_completions_for "math stats average"' -l 'version' -d 'Show the version.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats average"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats stdev"' -l 'version' -d 'Show the version.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats stdev"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 1' -fka 'alphabet alligator branch braggart' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 2' -fka '(__math_custom_completion ---completion stats quantiles -- positional@1 (count (__math_tokens -pc)) (__math_tokens -tC))' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles" 3' -fka '(__math_custom_completion ---completion stats quantiles -- positional@2)' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'file' -rfa '(set -l exts \'txt\' \'md\';for p in (string match -e -- \'*/\' (commandline -t);or printf \n)*.{$exts};printf %s\n $p;end;__fish_complete_directories (commandline -t) \'\')' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'directory' -rfa '(__math_complete_directories)' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'shell' -rfka '(head -100 \'/usr/share/dict/words\' | tail -50)' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'custom' -rfka '(__math_custom_completion ---completion stats quantiles -- --custom (count (__math_tokens -pc)) (__math_tokens -tC))' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'custom-deprecated' -rfka '(__math_custom_completion ---completion stats quantiles -- --custom-deprecated)' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -l 'version' -d 'Show the version.' -complete -c 'math' -n '__math_should_offer_completions_for "math stats quantiles"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'math' -n '__math_should_offer_completions_for "math help"' -l 'version' -d 'Show the version.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math" version' -l 'version' -d 'Show the version.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math" 1' -fa 'add' -d 'Print the sum of the values.' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math" 1' -fa 'multiply' -d 'Print the product of the values.' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math" 1' -fa 'stats' -d 'Calculate descriptive statistics.' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math" 1' -fa 'help' -d 'Show subcommand help information.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math add" hex-output x' -l 'hex-output' -s 'x' -d 'Use hexadecimal notation for the result.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math add" version' -l 'version' -d 'Show the version.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math add" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math multiply" hex-output x' -l 'hex-output' -s 'x' -d 'Use hexadecimal notation for the result.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math multiply" version' -l 'version' -d 'Show the version.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math multiply" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats" version' -l 'version' -d 'Show the version.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math stats" 1' -fa 'average' -d 'Print the average of the values.' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math stats" 1' -fa 'stdev' -d 'Print the standard deviation of the values.' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math stats" 1' -fa 'quantiles' -d 'Print the quantiles of the values (TBD).' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats average" kind' -l 'kind' -d 'The kind of average to provide.' -rfka 'mean median mode' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats average" version' -l 'version' -d 'Show the version.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats average" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats stdev" version' -l 'version' -d 'Show the version.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats stdev" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math stats quantiles" 1' -fka 'alphabet alligator branch braggart' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math stats quantiles" 2' -fka '(__math_custom_completion ---completion stats quantiles -- positional@1 (count (__math_tokens -pc)) (__math_tokens -tC))' +complete -c 'math' -n '__math_should_offer_completions_for_positional "math stats quantiles" 3' -fka '(__math_custom_completion ---completion stats quantiles -- positional@2)' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" file' -l 'file' -rfa '(set -l exts \'txt\' \'md\';for p in (string match -e -- \'*/\' (commandline -t);or printf \n)*.{$exts};printf %s\n $p;end;__fish_complete_directories (commandline -t) \'\')' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" directory' -l 'directory' -rfa '(__math_complete_directories)' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" shell' -l 'shell' -rfka '(head -100 \'/usr/share/dict/words\' | tail -50)' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" custom' -l 'custom' -rfka '(__math_custom_completion ---completion stats quantiles -- --custom (count (__math_tokens -pc)) (__math_tokens -tC))' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" custom-deprecated' -l 'custom-deprecated' -rfka '(__math_custom_completion ---completion stats quantiles -- --custom-deprecated)' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" version' -l 'version' -d 'Show the version.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math stats quantiles" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'math' -n '__math_should_offer_completions_for_flags_or_options "math help" version' -l 'version' -d 'Show the version.' diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish index 7c52c74bf..df9e309b8 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish @@ -1,15 +1,33 @@ -function __base-test_should_offer_completions_for -a expected_commands expected_positional_index positional_index_comparison - set -l unparsed_tokens (__base-test_tokens -pc) +function __base-test_should_offer_completions_for_flags_or_options -a expected_commands + set -l non_repeating_flags_or_options $argv[2..] + + set -l non_repeating_flags_or_options_absent 0 set -l positional_index 0 set -l commands + __base-test_parse_tokens + test "$commands" = "$expected_commands"; and return $non_repeating_flags_or_options_absent +end +function __base-test_should_offer_completions_for_positional -a expected_commands expected_positional_index positional_index_comparison if test -z $positional_index_comparison set positional_index_comparison -eq end + set -l non_repeating_flags_or_options + set -l non_repeating_flags_or_options_absent 0 + set -l positional_index 0 + set -l commands + __base-test_parse_tokens + test "$commands" = "$expected_commands" -a \( "$positional_index" "$positional_index_comparison" "$expected_positional_index" \) +end + +function __base-test_parse_tokens -S + set -l unparsed_tokens (__base-test_tokens -pc) + set -l present_flags_and_options + switch $unparsed_tokens[1] case 'base-test' - __base-test_parse_subcommand 2 'name=' 'kind=' 'other-kind=' 'path1=' 'path2=' 'path3=' 'one' 'two' 'custom-three' 'kind-counter' 'rep1=' 'r/rep2=' 'h/help' + __base-test_parse_subcommand 2 'name=' 'kind=' 'other-kind=' 'path1=' 'path2=' 'path3=' 'one' 'two' 'custom-three' 'kind-counter' 'rep1=+' 'r/rep2=+' 'h/help' switch $unparsed_tokens[1] case 'sub-command' __base-test_parse_subcommand 0 'h/help' @@ -19,8 +37,6 @@ function __base-test_should_offer_completions_for -a expected_commands expected_ __base-test_parse_subcommand -r 1 end end - - test "$commands" = "$expected_commands" -a \( -z "$expected_positional_index" -o "$positional_index" "$positional_index_comparison" "$expected_positional_index" \) end function __base-test_tokens @@ -31,9 +47,8 @@ function __base-test_tokens end end -function __base-test_parse_subcommand -S +function __base-test_parse_subcommand -S -a positional_count argparse -s r -- $argv - set -l positional_count $argv[1] set -l option_specs $argv[2..] set -a commands $unparsed_tokens[1] @@ -45,8 +60,16 @@ function __base-test_parse_subcommand -S argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null set unparsed_tokens $argv set positional_index (math $positional_index + 1) + + for non_repeating_flag_or_option in $non_repeating_flags_or_options + if set -ql _flag_$non_repeating_flag_or_option + set non_repeating_flags_or_options_absent 1 + break + end + end + if test (count $unparsed_tokens) -eq 0 -o \( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \) - return 0 + break end set -e unparsed_tokens[1] end @@ -72,25 +95,25 @@ function __base-test_custom_completion end complete -c 'base-test' -f -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'name' -d 'The user\'s name.' -rfka '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'kind' -rfka 'one two custom-three' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'other-kind' -rfka 'b1_fish b2_fish b3_fish' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'path1' -rF -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'path2' -rF -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'path3' -rfka 'c1_fish c2_fish c3_fish' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'one' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'two' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'custom-three' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'kind-counter' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -l 'rep1' -rfka '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -s 'r' -l 'rep2' -rfka '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 1' -fka '(__base-test_custom_completion ---completion -- positional@0 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 2' -fka '(__base-test_custom_completion ---completion -- positional@1 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'sub-command' -d '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'escaped-command' -d '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test" 3' -fa 'help' -d 'Show subcommand help information.' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test sub-command"' -s 'h' -l 'help' -d 'Show help information.' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command"' -l 'o:n[e' -d 'Escaped chars: \'[]\\.' -rfka '' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command" 1' -fka '(__base-test_custom_completion ---completion escaped-command -- positional@0 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' -complete -c 'base-test' -n '__base-test_should_offer_completions_for "base-test escaped-command"' -s 'h' -l 'help' -d 'Show help information.' \ No newline at end of file +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" name' -l 'name' -d 'The user\'s name.' -rfka '' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" kind' -l 'kind' -rfka 'one two custom-three' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" other-kind' -l 'other-kind' -rfka 'b1_fish b2_fish b3_fish' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" path1' -l 'path1' -rF +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" path2' -l 'path2' -rF +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" path3' -l 'path3' -rfka 'c1_fish c2_fish c3_fish' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" one' -l 'one' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" two' -l 'two' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" custom-three' -l 'custom-three' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test"' -l 'kind-counter' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test"' -l 'rep1' -rfka '' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test"' -s 'r' -l 'rep2' -rfka '' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_positional "base-test" 1' -fka '(__base-test_custom_completion ---completion -- positional@0 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_positional "base-test" 2' -fka '(__base-test_custom_completion ---completion -- positional@1 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_positional "base-test" 3' -fa 'sub-command' -d '' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_positional "base-test" 3' -fa 'escaped-command' -d '' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_positional "base-test" 3' -fa 'help' -d 'Show subcommand help information.' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test sub-command" h help' -s 'h' -l 'help' -d 'Show help information.' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test escaped-command" o:n[e' -l 'o:n[e' -d 'Escaped chars: \'[]\\.' -rfka '' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_positional "base-test escaped-command" 1' -fka '(__base-test_custom_completion ---completion escaped-command -- positional@0 (count (__base-test_tokens -pc)) (__base-test_tokens -tC))' +complete -c 'base-test' -n '__base-test_should_offer_completions_for_flags_or_options "base-test escaped-command" h help' -s 'h' -l 'help' -d 'Show help information.' \ No newline at end of file From 070273b7be96399400063dde26b292cb62a5511a Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:35:43 -0400 Subject: [PATCH 8/9] Use prior bindings to improve performance in bash completion script generation. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/BashCompletionsGenerator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 8e1fca2ed..9bba5dcc4 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -234,12 +234,12 @@ extension CommandInfoV0 { // Generate the case pattern-matching statements for option values. // If there aren't any, skip the case block altogether. let optionHandlers = - arguments.compactMap { arg in + options.compactMap { arg in guard arg.kind != .flag else { return nil } let words = arg.completionWords guard !words.isEmpty else { return nil } return """ - \(arg.completionWords.map { "'\($0.shellEscapeForSingleQuotedString())'" }.joined(separator: "|"))) + \(words.map { "'\($0.shellEscapeForSingleQuotedString())'" }.joined(separator: "|"))) \(valueCompletion(arg).indentingEachLine(by: 8))\ return ;; From 3829812cb9bdae94212b443e277415226780c3b0 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:45:38 -0400 Subject: [PATCH 9/9] In bash completion scripts, replace max int64 magic number with -1 to accept all subsequent non-flag/non-option/non-option-value tokens as positionals. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 4 ++-- .../Snapshots/testMathBashCompletionScript().bash | 14 +++++++------- .../Snapshots/testBase_Bash().bash | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 9bba5dcc4..16dfa1f5f 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -126,7 +126,7 @@ extension CommandInfoV0 { fi # ${word} is neither a flag, nor an option, nor an option value - if [[ "${positional_number}" -lt "${positional_count}" ]]; then + if [[ "${positional_number}" -lt "${positional_count}" || "${positional_count}" -lt 0 ]]; then # ${word} is a positional ((positional_number++)) unset "unparsed_words[${word_index}]" @@ -226,7 +226,7 @@ extension CommandInfoV0 { \(declareTopLevelArray)repeating_options=(\(options.filter(\.isRepeating).flatMap(\.completionWords).joined(separator: " "))) \(declareTopLevelArray)non_repeating_options=(\(options.filter { !$0.isRepeating }.flatMap(\.completionWords).joined(separator: " "))) \(offerFlagsOptionsFunctionName) \ - \(positionalArguments.contains { $0.isRepeating } ? 9_223_372_036_854_775_807 : positionalArguments.count) + \(positionalArguments.contains { $0.isRepeating } ? -1 : positionalArguments.count) """ } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index f8db8d4b7..a4cf00895 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -100,7 +100,7 @@ __math_offer_flags_options() { fi # ${word} is neither a flag, nor an option, nor an option value - if [[ "${positional_number}" -lt "${positional_count}" ]]; then + if [[ "${positional_number}" -lt "${positional_count}" || "${positional_count}" -lt 0 ]]; then # ${word} is a positional ((positional_number++)) unset "unparsed_words[${word_index}]" @@ -187,7 +187,7 @@ _math_add() { non_repeating_flags=(--hex-output -x --version -h --help) repeating_options=() non_repeating_options=() - __math_offer_flags_options 9223372036854775807 + __math_offer_flags_options -1 } _math_multiply() { @@ -195,7 +195,7 @@ _math_multiply() { non_repeating_flags=(--hex-output -x --version -h --help) repeating_options=() non_repeating_options=() - __math_offer_flags_options 9223372036854775807 + __math_offer_flags_options -1 } _math_stats() { @@ -226,7 +226,7 @@ _math_stats_average() { non_repeating_flags=(--version -h --help) repeating_options=() non_repeating_options=(--kind) - __math_offer_flags_options 9223372036854775807 + __math_offer_flags_options -1 # Offer option value completions case "${prev}" in @@ -242,7 +242,7 @@ _math_stats_stdev() { non_repeating_flags=(--version -h --help) repeating_options=() non_repeating_options=() - __math_offer_flags_options 9223372036854775807 + __math_offer_flags_options -1 } _math_stats_quantiles() { @@ -250,7 +250,7 @@ _math_stats_quantiles() { non_repeating_flags=(--version -h --help) repeating_options=() non_repeating_options=(--file --directory --shell --custom --custom-deprecated) - __math_offer_flags_options 9223372036854775807 + __math_offer_flags_options -1 # Offer option value completions case "${prev}" in @@ -298,7 +298,7 @@ _math_help() { non_repeating_flags=(--version) repeating_options=() non_repeating_options=() - __math_offer_flags_options 9223372036854775807 + __math_offer_flags_options -1 } complete -o filenames -F _math math diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 762d10c1b..d9b6394c0 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -100,7 +100,7 @@ __base-test_offer_flags_options() { fi # ${word} is neither a flag, nor an option, nor an option value - if [[ "${positional_number}" -lt "${positional_count}" ]]; then + if [[ "${positional_number}" -lt "${positional_count}" || "${positional_count}" -lt 0 ]]; then # ${word} is a positional ((positional_number++)) unset "unparsed_words[${word_index}]"