From 1d93c4050160997519d31cd0e7e5fb90f9a42f77 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:13:45 -0500 Subject: [PATCH 01/28] Restrict access to symbols in BashCompletionsGenerator.swift. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 0c635d399..025eb9e11 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -25,7 +25,7 @@ struct BashCompletionsGenerator { } /// Generates a Bash completion function for the last command in the given list. - fileprivate static func generateCompletionFunction( + private static func generateCompletionFunction( _ commands: [ParsableCommand.Type] ) -> String { guard let type = commands.last else { @@ -131,7 +131,7 @@ struct BashCompletionsGenerator { } /// Returns the option and flag names that can be top-level completions. - fileprivate static func generateArgumentWords( + private static func generateArgumentWords( _ commands: [ParsableCommand.Type] ) -> [String] { commands @@ -142,7 +142,7 @@ struct BashCompletionsGenerator { /// Returns additional top-level completions from positional arguments. /// /// These consist of completions that are defined as `.list` or `.custom`. - fileprivate static func generateArgumentCompletions( + private static func generateArgumentCompletions( _ commands: [ParsableCommand.Type] ) -> [String] { guard let type = commands.last else { return [] } @@ -166,7 +166,7 @@ struct BashCompletionsGenerator { } /// Returns the case-matching statements for supplying completions after an option or flag. - fileprivate static func generateOptionHandlers( + private static func generateOptionHandlers( _ commands: [ParsableCommand.Type] ) -> String { guard let type = commands.last else { return "" } From b006f5c346c6ea941b25eb91f353e43758fd6397 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 18:21:30 -0500 Subject: [PATCH 02/28] Use key path instead of closure in BashCompletionsGenerator.swift. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/BashCompletionsGenerator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 025eb9e11..d73b55e2c 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -193,7 +193,7 @@ extension ArgumentDefinition { /// Returns the different completion names for this argument. fileprivate func bashCompletionWords() -> [String] { help.visibility.base == .default - ? names.map { $0.synopsisString } + ? names.map(\.synopsisString) : [] } From a1e8d4ad2865fb34ef4547ae9a9852e4fadd33cf Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 07:45:37 -0500 Subject: [PATCH 03/28] Remove extraneous bash spacing. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 14 +++---- .../testMathBashCompletionScript().bash | 40 +++++++++---------- .../Snapshots/testBase_Bash().bash | 26 ++++++------ 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index d73b55e2c..64eb850df 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -82,7 +82,7 @@ struct BashCompletionsGenerator { result += """ if [[ $COMP_CWORD == "\(dollarOne)" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi @@ -119,7 +119,7 @@ struct BashCompletionsGenerator { // Finish off the function. result += """ - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } """ @@ -212,7 +212,7 @@ extension ArgumentDefinition { if declare -F _filedir >/dev/null; then _filedir else - COMPREPLY=( $(compgen -f -- "$cur") ) + COMPREPLY=($(compgen -f -- "$cur")) fi """ @@ -239,21 +239,21 @@ extension ArgumentDefinition { if declare -F _filedir >/dev/null; then _filedir -d else - COMPREPLY=( $(compgen -d -- "$cur") ) + COMPREPLY=($(compgen -d -- "$cur")) fi """ case .list(let list): return - #"COMPREPLY=( $(compgen -W "\#(list.joined(separator: " "))" -- "$cur") )"# + #"COMPREPLY=($(compgen -W "\#(list.joined(separator: " "))" -- "$cur"))"# case .shellCommand(let command): - return "COMPREPLY=( $(\(command)) )" + return "COMPREPLY=($(\(command)))" case .custom: // Generate a call back into the command to retrieve a completions list return - #"COMPREPLY=( $(compgen -W "$("${COMP_WORDS[0]}" \#(customCompletionCall(commands)) "${COMP_WORDS[@]}")" -- "$cur") )"# + #"COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" \#(customCompletionCall(commands)) "${COMP_WORDS[@]}")" -- "$cur"))"# } } } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 815cbed80..8121163c6 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -9,7 +9,7 @@ _math() { COMPREPLY=() opts="--version -h --help add multiply stats help" if [[ $COMP_CWORD == "1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi case ${COMP_WORDS[1]} in @@ -30,28 +30,28 @@ _math() { return ;; esac - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _math_add() { opts="--hex-output -x --version -h --help" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _math_multiply() { opts="--hex-output -x --version -h --help" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _math_stats() { opts="--version -h --help average stdev quantiles" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi case ${COMP_WORDS[$1]} in @@ -68,36 +68,36 @@ _math_stats() { return ;; esac - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _math_stats_average() { opts="--kind --version -h --help" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi case $prev in --kind) - COMPREPLY=( $(compgen -W "mean median mode" -- "$cur") ) + COMPREPLY=($(compgen -W "mean median mode" -- "$cur")) return ;; esac - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _math_stats_stdev() { opts="--version -h --help" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _math_stats_quantiles() { opts="--file --directory --shell --custom --version -h --help" opts="$opts alphabet alligator branch braggart" opts="$opts $("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi case $prev in @@ -123,28 +123,28 @@ _math_stats_quantiles() { if declare -F _filedir >/dev/null; then _filedir -d else - COMPREPLY=( $(compgen -d -- "$cur") ) + COMPREPLY=($(compgen -d -- "$cur")) fi return ;; --shell) - COMPREPLY=( $(head -100 /usr/share/dict/words | tail -50) ) + COMPREPLY=($(head -100 /usr/share/dict/words | tail -50)) return ;; --custom) - COMPREPLY=( $(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "$cur") ) + COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "$cur")) return ;; esac - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _math_help() { opts="--version" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 599a68f4f..ecbd1b10c 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -11,7 +11,7 @@ _base_test() { opts="$opts $("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" opts="$opts $("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" if [[ $COMP_CWORD == "1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi case $prev in @@ -20,18 +20,18 @@ _base_test() { return ;; --kind) - COMPREPLY=( $(compgen -W "one two custom-three" -- "$cur") ) + COMPREPLY=($(compgen -W "one two custom-three" -- "$cur")) return ;; --other-kind) - COMPREPLY=( $(compgen -W "b1_bash b2_bash b3_bash" -- "$cur") ) + COMPREPLY=($(compgen -W "b1_bash b2_bash b3_bash" -- "$cur")) return ;; --path1) if declare -F _filedir >/dev/null; then _filedir else - COMPREPLY=( $(compgen -f -- "$cur") ) + COMPREPLY=($(compgen -f -- "$cur")) fi return ;; @@ -39,12 +39,12 @@ _base_test() { if declare -F _filedir >/dev/null; then _filedir else - COMPREPLY=( $(compgen -f -- "$cur") ) + COMPREPLY=($(compgen -f -- "$cur")) fi return ;; --path3) - COMPREPLY=( $(compgen -W "c1_bash c2_bash c3_bash" -- "$cur") ) + COMPREPLY=($(compgen -W "c1_bash c2_bash c3_bash" -- "$cur")) return ;; --rep1) @@ -70,21 +70,21 @@ _base_test() { return ;; esac - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _base_test_sub_command() { opts="-h --help" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _base_test_escaped_command() { opts="--one -h --help" opts="$opts $("${COMP_WORDS[0]}" ---completion escaped-command -- two "${COMP_WORDS[@]}")" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi case $prev in @@ -93,15 +93,15 @@ _base_test_escaped_command() { return ;; esac - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } _base_test_help() { opts="" if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi - COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + COMPREPLY=($(compgen -W "$opts" -- "$cur")) } From 633631d5ea2f844c1dce87531f6d9c9673796158 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 07:53:41 -0500 Subject: [PATCH 04/28] Do not indent bash cases. Standardize bash indents. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 62 +++++----- .../testMathBashCompletionScript().bash | 112 +++++++++--------- .../Snapshots/testBase_Bash().bash | 86 +++++++------- 3 files changed, 130 insertions(+), 130 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 64eb850df..e683a5333 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -63,14 +63,14 @@ struct BashCompletionsGenerator { // that other command functions don't need. if isRootCommand { result += """ - export \(CompletionShell.shellEnvironmentVariableName)=bash - \(CompletionShell.shellVersionEnvironmentVariableName)="$(IFS='.'; printf %s "${BASH_VERSINFO[*]}")" - export \(CompletionShell.shellVersionEnvironmentVariableName) - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - COMPREPLY=() - - """.indentingEachLine(by: 4) + export \(CompletionShell.shellEnvironmentVariableName)=bash + \(CompletionShell.shellVersionEnvironmentVariableName)="$(IFS='.'; printf %s "${BASH_VERSINFO[*]}")" + export \(CompletionShell.shellVersionEnvironmentVariableName) + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + + """ } // Start by declaring a local var for the top-level completions. @@ -93,10 +93,11 @@ struct BashCompletionsGenerator { let optionHandlers = generateOptionHandlers(commands) if !optionHandlers.isEmpty { result += """ - case $prev in - \(optionHandlers.indentingEachLine(by: 4)) - esac - """.indentingEachLine(by: 4) + "\n" + case $prev in + \(optionHandlers) + esac + + """ } // Build out completions for the subcommands. @@ -106,13 +107,12 @@ struct BashCompletionsGenerator { result += " case ${COMP_WORDS[\(dollarOne)]} in\n" for subcommand in subcommands { result += """ - (\(subcommand._commandName)) - \(functionName)_\(subcommand._commandName) \(subcommandArgument) - return - ;; + (\(subcommand._commandName)) + \(functionName)_\(subcommand._commandName) \(subcommandArgument) + return + ;; """ - .indentingEachLine(by: 8) } result += " esac\n" } @@ -179,10 +179,10 @@ struct BashCompletionsGenerator { if arg.isNullary { return nil } return """ - \(arg.bashCompletionWords().joined(separator: "|"))) - \(arg.bashValueCompletion(commands).indentingEachLine(by: 4)) - return - ;; + \(arg.bashCompletionWords().joined(separator: "|"))) + \(arg.bashValueCompletion(commands).indentingEachLine(by: 8)) + return + ;; """ } .joined(separator: "\n") @@ -210,9 +210,9 @@ extension ArgumentDefinition { case .file(let extensions) where extensions.isEmpty: return """ if declare -F _filedir >/dev/null; then - _filedir + _filedir else - COMPREPLY=($(compgen -f -- "$cur")) + COMPREPLY=($(compgen -f -- "$cur")) fi """ @@ -224,22 +224,22 @@ extension ArgumentDefinition { return """ if declare -F _filedir >/dev/null; then - \(safeExts.map { "_filedir '\($0)'" }.joined(separator:"\n ")) - _filedir -d + \(safeExts.map { "_filedir '\($0)'" }.joined(separator:"\n ")) + _filedir -d else - COMPREPLY=( - \(safeExts.map { "$(compgen -f -X '!*.\($0)' -- \"$cur\")" }.joined(separator: "\n ")) - $(compgen -d -- "$cur") - ) + COMPREPLY=( + \(safeExts.map { "$(compgen -f -X '!*.\($0)' -- \"$cur\")" }.joined(separator: "\n ")) + $(compgen -d -- "$cur") + ) fi """ case .directory: return """ if declare -F _filedir >/dev/null; then - _filedir -d + _filedir -d else - COMPREPLY=($(compgen -d -- "$cur")) + COMPREPLY=($(compgen -d -- "$cur")) fi """ diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 8121163c6..c4bd4c036 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -13,22 +13,22 @@ _math() { return fi case ${COMP_WORDS[1]} in - (add) - _math_add 2 - return - ;; - (multiply) - _math_multiply 2 - return - ;; - (stats) - _math_stats 2 - return - ;; - (help) - _math_help 2 - return - ;; + (add) + _math_add 2 + return + ;; + (multiply) + _math_multiply 2 + return + ;; + (stats) + _math_stats 2 + return + ;; + (help) + _math_help 2 + return + ;; esac COMPREPLY=($(compgen -W "$opts" -- "$cur")) } @@ -55,18 +55,18 @@ _math_stats() { return fi case ${COMP_WORDS[$1]} in - (average) - _math_stats_average $(($1+1)) - return - ;; - (stdev) - _math_stats_stdev $(($1+1)) - return - ;; - (quantiles) - _math_stats_quantiles $(($1+1)) - return - ;; + (average) + _math_stats_average $(($1+1)) + return + ;; + (stdev) + _math_stats_stdev $(($1+1)) + return + ;; + (quantiles) + _math_stats_quantiles $(($1+1)) + return + ;; esac COMPREPLY=($(compgen -W "$opts" -- "$cur")) } @@ -77,9 +77,9 @@ _math_stats_average() { return fi case $prev in - --kind) - COMPREPLY=($(compgen -W "mean median mode" -- "$cur")) - return + --kind) + COMPREPLY=($(compgen -W "mean median mode" -- "$cur")) + return ;; esac COMPREPLY=($(compgen -W "$opts" -- "$cur")) @@ -101,39 +101,39 @@ _math_stats_quantiles() { return fi case $prev in - --file) - if declare -F _filedir >/dev/null; then - _filedir 'txt' - _filedir 'md' - _filedir 'TXT' - _filedir 'MD' - _filedir -d - else - COMPREPLY=( + --file) + if declare -F _filedir >/dev/null; then + _filedir 'txt' + _filedir 'md' + _filedir 'TXT' + _filedir 'MD' + _filedir -d + else + COMPREPLY=( $(compgen -f -X '!*.txt' -- "$cur") $(compgen -f -X '!*.md' -- "$cur") $(compgen -f -X '!*.TXT' -- "$cur") $(compgen -f -X '!*.MD' -- "$cur") $(compgen -d -- "$cur") - ) - fi - return + ) + fi + return ;; - --directory) - if declare -F _filedir >/dev/null; then - _filedir -d - else - COMPREPLY=($(compgen -d -- "$cur")) - fi - return + --directory) + if declare -F _filedir >/dev/null; then + _filedir -d + else + COMPREPLY=($(compgen -d -- "$cur")) + fi + return ;; - --shell) - COMPREPLY=($(head -100 /usr/share/dict/words | tail -50)) - return + --shell) + COMPREPLY=($(head -100 /usr/share/dict/words | tail -50)) + return ;; - --custom) - COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "$cur")) - return + --custom) + COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "$cur")) + return ;; esac COMPREPLY=($(compgen -W "$opts" -- "$cur")) diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index ecbd1b10c..5a14c6969 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -15,60 +15,60 @@ _base_test() { return fi case $prev in - --name) + --name) - return + return ;; - --kind) - COMPREPLY=($(compgen -W "one two custom-three" -- "$cur")) - return + --kind) + COMPREPLY=($(compgen -W "one two custom-three" -- "$cur")) + return ;; - --other-kind) - COMPREPLY=($(compgen -W "b1_bash b2_bash b3_bash" -- "$cur")) - return + --other-kind) + COMPREPLY=($(compgen -W "b1_bash b2_bash b3_bash" -- "$cur")) + return ;; - --path1) - if declare -F _filedir >/dev/null; then - _filedir - else - COMPREPLY=($(compgen -f -- "$cur")) - fi - return + --path1) + if declare -F _filedir >/dev/null; then + _filedir + else + COMPREPLY=($(compgen -f -- "$cur")) + fi + return ;; - --path2) - if declare -F _filedir >/dev/null; then - _filedir - else - COMPREPLY=($(compgen -f -- "$cur")) - fi - return + --path2) + if declare -F _filedir >/dev/null; then + _filedir + else + COMPREPLY=($(compgen -f -- "$cur")) + fi + return ;; - --path3) - COMPREPLY=($(compgen -W "c1_bash c2_bash c3_bash" -- "$cur")) - return + --path3) + COMPREPLY=($(compgen -W "c1_bash c2_bash c3_bash" -- "$cur")) + return ;; - --rep1) + --rep1) - return + return ;; - -r|--rep2) + -r|--rep2) - return + return ;; esac case ${COMP_WORDS[1]} in - (sub-command) - _base_test_sub-command 2 - return - ;; - (escaped-command) - _base_test_escaped-command 2 - return - ;; - (help) - _base_test_help 2 - return - ;; + (sub-command) + _base_test_sub-command 2 + return + ;; + (escaped-command) + _base_test_escaped-command 2 + return + ;; + (help) + _base_test_help 2 + return + ;; esac COMPREPLY=($(compgen -W "$opts" -- "$cur")) } @@ -88,9 +88,9 @@ _base_test_escaped_command() { return fi case $prev in - --one) + --one) - return + return ;; esac COMPREPLY=($(compgen -W "$opts" -- "$cur")) From 616b00136c849972a998ba3dcb75c706298301eb Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 07:56:59 -0500 Subject: [PATCH 05/28] Do not prefix bash cases with an open parenthesis. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 2 +- .../Snapshots/testMathBashCompletionScript().bash | 14 +++++++------- .../Snapshots/testBase_Bash().bash | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index e683a5333..c7757b282 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -107,7 +107,7 @@ struct BashCompletionsGenerator { result += " case ${COMP_WORDS[\(dollarOne)]} in\n" for subcommand in subcommands { result += """ - (\(subcommand._commandName)) + \(subcommand._commandName)) \(functionName)_\(subcommand._commandName) \(subcommandArgument) return ;; diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index c4bd4c036..605f61e3b 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -13,19 +13,19 @@ _math() { return fi case ${COMP_WORDS[1]} in - (add) + add) _math_add 2 return ;; - (multiply) + multiply) _math_multiply 2 return ;; - (stats) + stats) _math_stats 2 return ;; - (help) + help) _math_help 2 return ;; @@ -55,15 +55,15 @@ _math_stats() { return fi case ${COMP_WORDS[$1]} in - (average) + average) _math_stats_average $(($1+1)) return ;; - (stdev) + stdev) _math_stats_stdev $(($1+1)) return ;; - (quantiles) + quantiles) _math_stats_quantiles $(($1+1)) return ;; diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 5a14c6969..52999fc87 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -57,15 +57,15 @@ _base_test() { ;; esac case ${COMP_WORDS[1]} in - (sub-command) + sub-command) _base_test_sub-command 2 return ;; - (escaped-command) + escaped-command) _base_test_escaped-command 2 return ;; - (help) + help) _base_test_help 2 return ;; From f76febf566b27fb498bae24828ef713faf69ba91 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 08:11:59 -0500 Subject: [PATCH 06/28] Brace & quote bash variable uses. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 28 +++---- .../testMathBashCompletionScript().bash | 82 +++++++++---------- .../Snapshots/testBase_Bash().bash | 46 +++++------ 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index c7757b282..df8bc8d46 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -35,8 +35,8 @@ struct BashCompletionsGenerator { // The root command gets a different treatment for the parsing index. let isRootCommand = commands.count == 1 - let dollarOne = isRootCommand ? "1" : "$1" - let subcommandArgument = isRootCommand ? "2" : "$(($1+1))" + let dollarOne = isRootCommand ? "1" : "${1}" + let subcommandArgument = isRootCommand ? "2" : "$((${1}+1))" // Include 'help' in the list of subcommands for the root command. var subcommands = type.configuration.subcommands @@ -77,12 +77,12 @@ struct BashCompletionsGenerator { // Return immediately if the completion matching hasn't moved further. result += " opts=\"\(completionWords.joined(separator: " "))\"\n" for line in additionalCompletions { - result += " opts=\"$opts \(line)\"\n" + result += " opts=\"${opts} \(line)\"\n" } result += """ - if [[ $COMP_CWORD == "\(dollarOne)" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "\(dollarOne)" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi @@ -93,7 +93,7 @@ struct BashCompletionsGenerator { let optionHandlers = generateOptionHandlers(commands) if !optionHandlers.isEmpty { result += """ - case $prev in + case "${prev}" in \(optionHandlers) esac @@ -104,7 +104,7 @@ struct BashCompletionsGenerator { if !subcommands.isEmpty { // Subcommands have their own case statement that delegates out to // the subcommand completion functions. - result += " case ${COMP_WORDS[\(dollarOne)]} in\n" + result += " case \"${COMP_WORDS[\(dollarOne)]}\" in\n" for subcommand in subcommands { result += """ \(subcommand._commandName)) @@ -119,7 +119,7 @@ struct BashCompletionsGenerator { // Finish off the function. result += """ - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } """ @@ -212,7 +212,7 @@ extension ArgumentDefinition { if declare -F _filedir >/dev/null; then _filedir else - COMPREPLY=($(compgen -f -- "$cur")) + COMPREPLY=($(compgen -f -- "${cur}")) fi """ @@ -228,8 +228,8 @@ extension ArgumentDefinition { _filedir -d else COMPREPLY=( - \(safeExts.map { "$(compgen -f -X '!*.\($0)' -- \"$cur\")" }.joined(separator: "\n ")) - $(compgen -d -- "$cur") + \(safeExts.map { "$(compgen -f -X '!*.\($0)' -- \"${cur}\")" }.joined(separator: "\n ")) + $(compgen -d -- "${cur}") ) fi """ @@ -239,13 +239,13 @@ extension ArgumentDefinition { if declare -F _filedir >/dev/null; then _filedir -d else - COMPREPLY=($(compgen -d -- "$cur")) + COMPREPLY=($(compgen -d -- "${cur}")) fi """ case .list(let list): return - #"COMPREPLY=($(compgen -W "\#(list.joined(separator: " "))" -- "$cur"))"# + #"COMPREPLY=($(compgen -W "\#(list.joined(separator: " "))" -- "${cur}"))"# case .shellCommand(let command): return "COMPREPLY=($(\(command)))" @@ -253,7 +253,7 @@ extension ArgumentDefinition { case .custom: // Generate a call back into the command to retrieve a completions list return - #"COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" \#(customCompletionCall(commands)) "${COMP_WORDS[@]}")" -- "$cur"))"# + #"COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" \#(customCompletionCall(commands)) "${COMP_WORDS[@]}")" -- "${cur}"))"# } } } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 605f61e3b..a46132378 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -8,11 +8,11 @@ _math() { prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() opts="--version -h --help add multiply stats help" - if [[ $COMP_CWORD == "1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "1" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - case ${COMP_WORDS[1]} in + case "${COMP_WORDS[1]}" in add) _math_add 2 return @@ -30,77 +30,77 @@ _math() { return ;; esac - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _math_add() { opts="--hex-output -x --version -h --help" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _math_multiply() { opts="--hex-output -x --version -h --help" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _math_stats() { opts="--version -h --help average stdev quantiles" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - case ${COMP_WORDS[$1]} in + case "${COMP_WORDS[${1}]}" in average) - _math_stats_average $(($1+1)) + _math_stats_average $((${1}+1)) return ;; stdev) - _math_stats_stdev $(($1+1)) + _math_stats_stdev $((${1}+1)) return ;; quantiles) - _math_stats_quantiles $(($1+1)) + _math_stats_quantiles $((${1}+1)) return ;; esac - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _math_stats_average() { opts="--kind --version -h --help" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - case $prev in + case "${prev}" in --kind) - COMPREPLY=($(compgen -W "mean median mode" -- "$cur")) + COMPREPLY=($(compgen -W "mean median mode" -- "${cur}")) return ;; esac - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _math_stats_stdev() { opts="--version -h --help" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _math_stats_quantiles() { opts="--file --directory --shell --custom --version -h --help" - opts="$opts alphabet alligator branch braggart" - opts="$opts $("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + opts="${opts} alphabet alligator branch braggart" + opts="${opts} $("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - case $prev in + case "${prev}" in --file) if declare -F _filedir >/dev/null; then _filedir 'txt' @@ -110,11 +110,11 @@ _math_stats_quantiles() { _filedir -d else COMPREPLY=( - $(compgen -f -X '!*.txt' -- "$cur") - $(compgen -f -X '!*.md' -- "$cur") - $(compgen -f -X '!*.TXT' -- "$cur") - $(compgen -f -X '!*.MD' -- "$cur") - $(compgen -d -- "$cur") + $(compgen -f -X '!*.txt' -- "${cur}") + $(compgen -f -X '!*.md' -- "${cur}") + $(compgen -f -X '!*.TXT' -- "${cur}") + $(compgen -f -X '!*.MD' -- "${cur}") + $(compgen -d -- "${cur}") ) fi return @@ -123,7 +123,7 @@ _math_stats_quantiles() { if declare -F _filedir >/dev/null; then _filedir -d else - COMPREPLY=($(compgen -d -- "$cur")) + COMPREPLY=($(compgen -d -- "${cur}")) fi return ;; @@ -132,19 +132,19 @@ _math_stats_quantiles() { return ;; --custom) - COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "$cur")) + COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "${cur}")) return ;; esac - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _math_help() { opts="--version" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 52999fc87..cc83c249f 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -8,30 +8,30 @@ _base_test() { prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() opts="--name --kind --other-kind --path1 --path2 --path3 --one --two --three --kind-counter --rep1 -r --rep2 -h --help sub-command escaped-command help" - opts="$opts $("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" - opts="$opts $("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" - if [[ $COMP_CWORD == "1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + opts="${opts} $("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" + opts="${opts} $("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" + if [[ "${COMP_CWORD}" == "1" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - case $prev in + case "${prev}" in --name) return ;; --kind) - COMPREPLY=($(compgen -W "one two custom-three" -- "$cur")) + COMPREPLY=($(compgen -W "one two custom-three" -- "${cur}")) return ;; --other-kind) - COMPREPLY=($(compgen -W "b1_bash b2_bash b3_bash" -- "$cur")) + COMPREPLY=($(compgen -W "b1_bash b2_bash b3_bash" -- "${cur}")) return ;; --path1) if declare -F _filedir >/dev/null; then _filedir else - COMPREPLY=($(compgen -f -- "$cur")) + COMPREPLY=($(compgen -f -- "${cur}")) fi return ;; @@ -39,12 +39,12 @@ _base_test() { if declare -F _filedir >/dev/null; then _filedir else - COMPREPLY=($(compgen -f -- "$cur")) + COMPREPLY=($(compgen -f -- "${cur}")) fi return ;; --path3) - COMPREPLY=($(compgen -W "c1_bash c2_bash c3_bash" -- "$cur")) + COMPREPLY=($(compgen -W "c1_bash c2_bash c3_bash" -- "${cur}")) return ;; --rep1) @@ -56,7 +56,7 @@ _base_test() { return ;; esac - case ${COMP_WORDS[1]} in + case "${COMP_WORDS[1]}" in sub-command) _base_test_sub-command 2 return @@ -70,38 +70,38 @@ _base_test() { return ;; esac - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _base_test_sub_command() { opts="-h --help" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _base_test_escaped_command() { opts="--one -h --help" - opts="$opts $("${COMP_WORDS[0]}" ---completion escaped-command -- two "${COMP_WORDS[@]}")" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + opts="${opts} $("${COMP_WORDS[0]}" ---completion escaped-command -- two "${COMP_WORDS[@]}")" + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - case $prev in + case "${prev}" in --one) return ;; esac - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } _base_test_help() { opts="" - if [[ $COMP_CWORD == "$1" ]]; then - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + if [[ "${COMP_CWORD}" == "${1}" ]]; then + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) return fi - COMPREPLY=($(compgen -W "$opts" -- "$cur")) + COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } From 9c4f0ed251721e2bc1c9d211668a29741a658b4f Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 09:28:17 -0500 Subject: [PATCH 07/28] Remove extraneous bash blank line. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 22 ++++++++++++++----- .../Snapshots/testBase_Bash().bash | 4 ---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index df8bc8d46..8e8ca40d6 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -180,7 +180,7 @@ struct BashCompletionsGenerator { return """ \(arg.bashCompletionWords().joined(separator: "|"))) - \(arg.bashValueCompletion(commands).indentingEachLine(by: 8)) + \(arg.bashValueCompletion(commands).indentingEachLine(by: 8))\ return ;; """ @@ -214,6 +214,7 @@ extension ArgumentDefinition { else COMPREPLY=($(compgen -f -- "${cur}")) fi + """ case .file(let extensions): @@ -232,6 +233,7 @@ extension ArgumentDefinition { $(compgen -d -- "${cur}") ) fi + """ case .directory: @@ -241,19 +243,27 @@ extension ArgumentDefinition { else COMPREPLY=($(compgen -d -- "${cur}")) fi + """ case .list(let list): - return - #"COMPREPLY=($(compgen -W "\#(list.joined(separator: " "))" -- "${cur}"))"# + return """ + COMPREPLY=($(compgen -W "\(list.joined(separator: " "))" -- "${cur}")) + + """ case .shellCommand(let command): - return "COMPREPLY=($(\(command)))" + return """ + COMPREPLY=($(\(command))) + + """ case .custom: // Generate a call back into the command to retrieve a completions list - return - #"COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" \#(customCompletionCall(commands)) "${COMP_WORDS[@]}")" -- "${cur}"))"# + return """ + COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" \(customCompletionCall(commands)) "${COMP_WORDS[@]}")" -- "${cur}")) + + """ } } } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index cc83c249f..5a9bc84f9 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -16,7 +16,6 @@ _base_test() { fi case "${prev}" in --name) - return ;; --kind) @@ -48,11 +47,9 @@ _base_test() { return ;; --rep1) - return ;; -r|--rep2) - return ;; esac @@ -89,7 +86,6 @@ _base_test_escaped_command() { fi case "${prev}" in --one) - return ;; esac From 1e77cd40929e6365bb020c1720195f874e474c04 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 16:28:03 -0500 Subject: [PATCH 08/28] Improve bash escaping. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 8e8ca40d6..60bd27956 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -13,15 +13,13 @@ struct BashCompletionsGenerator { /// Generates a Bash completion script for the given command. static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { // TODO: Add a check to see if the command is installed where we expect? - let initialFunctionName = [type].completionFunctionName() - .makeSafeFunctionName - return """ - #!/bin/bash + """ + #!/bin/bash - \(generateCompletionFunction([type])) + \(generateCompletionFunction([type])) - complete -F \(initialFunctionName) \(type._commandName) - """ + complete -F \([type].completionFunctionName().shellEscapeForVariableName()) \(type._commandName) + """ } /// Generates a Bash completion function for the last command in the given list. @@ -31,7 +29,8 @@ struct BashCompletionsGenerator { guard let type = commands.last else { fatalError() } - let functionName = commands.completionFunctionName().makeSafeFunctionName + let functionName = + commands.completionFunctionName().shellEscapeForVariableName() // The root command gets a different treatment for the parsing index. let isRootCommand = commands.count == 1 @@ -49,8 +48,7 @@ struct BashCompletionsGenerator { // command — these are the dash-prefixed names of options and flags as well // as all the subcommand names. let completionWords = - generateArgumentWords(commands) - + subcommands.map { $0._commandName } + generateArgumentWords(commands) + subcommands.map { $0._commandName } // Generate additional top-level completions — these are completion lists // or custom function-based word lists from positional arguments. @@ -200,9 +198,9 @@ extension ArgumentDefinition { /// Returns the bash completions that can follow this argument's `--name`. /// /// Uses bash-completion for file and directory values if available. - fileprivate func bashValueCompletion(_ commands: [ParsableCommand.Type]) - -> String - { + fileprivate func bashValueCompletion( + _ commands: [ParsableCommand.Type] + ) -> String { switch completion.kind { case .default: return "" @@ -218,9 +216,7 @@ extension ArgumentDefinition { """ case .file(let extensions): - var safeExts = extensions.map { - String($0.flatMap { $0 == "'" ? ["\\", "'"] : [$0] }) - } + var safeExts = extensions.map { $0.shellEscapeForSingleQuotedString() } safeExts.append(contentsOf: safeExts.map { $0.uppercased() }) return """ @@ -267,9 +263,3 @@ extension ArgumentDefinition { } } } - -extension String { - var makeSafeFunctionName: String { - self.replacingOccurrences(of: "-", with: "_") - } -} From 8f467d212bab1b31d9618d22282e2fdeee601498 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 25 Jan 2025 23:09:58 -0500 Subject: [PATCH 09/28] Improve bash $cur, $prev, & $COMPREPLY. Use positional arguments passed by bash to the main completion function instead of reading from COMP_WORDS, as that can return the wrong info if completing an empty word before a non-empty word. Make $cur & $prev local & readonly. Remove unnecessary COMPREPLY=(). Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 7 ++++--- .../Snapshots/testMathBashCompletionScript().bash | 7 ++++--- .../ArgumentParserUnitTests/Snapshots/testBase_Bash().bash | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 60bd27956..1ff13adc7 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -64,9 +64,10 @@ struct BashCompletionsGenerator { export \(CompletionShell.shellEnvironmentVariableName)=bash \(CompletionShell.shellVersionEnvironmentVariableName)="$(IFS='.'; printf %s "${BASH_VERSINFO[*]}")" export \(CompletionShell.shellVersionEnvironmentVariableName) - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - COMPREPLY=() + + local -r cur="${2}" + local -r prev="${3}" + """ } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index a46132378..30931fb0f 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -4,9 +4,10 @@ _math() { export SAP_SHELL=bash SAP_SHELL_VERSION="$(IFS='.'; printf %s "${BASH_VERSINFO[*]}")" export SAP_SHELL_VERSION - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - COMPREPLY=() + + local -r cur="${2}" + local -r prev="${3}" + opts="--version -h --help add multiply stats help" if [[ "${COMP_CWORD}" == "1" ]]; then COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 5a9bc84f9..c7b465d80 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -4,9 +4,10 @@ _base_test() { export SAP_SHELL=bash SAP_SHELL_VERSION="$(IFS='.'; printf %s "${BASH_VERSINFO[*]}")" export SAP_SHELL_VERSION - cur="${COMP_WORDS[COMP_CWORD]}" - prev="${COMP_WORDS[COMP_CWORD-1]}" - COMPREPLY=() + + local -r cur="${2}" + local -r prev="${3}" + opts="--name --kind --other-kind --path1 --path2 --path3 --one --two --three --kind-counter --rep1 -r --rep2 -h --help sub-command escaped-command help" opts="${opts} $("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" opts="${opts} $("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" From efe71ada488924f931434a715187794cc5c6a461 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 26 Jan 2025 07:09:54 -0500 Subject: [PATCH 10/28] Improve bash $SAP_SHELL & $SAP_SHELL_VERSION. Make them local & readonly. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 7 ++++--- .../Snapshots/testMathBashCompletionScript().bash | 7 ++++--- .../ArgumentParserUnitTests/Snapshots/testBase_Bash().bash | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 1ff13adc7..11a0039d1 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -61,9 +61,10 @@ struct BashCompletionsGenerator { // that other command functions don't need. if isRootCommand { result += """ - export \(CompletionShell.shellEnvironmentVariableName)=bash - \(CompletionShell.shellVersionEnvironmentVariableName)="$(IFS='.'; printf %s "${BASH_VERSINFO[*]}")" - export \(CompletionShell.shellVersionEnvironmentVariableName) + local -xr \(CompletionShell.shellEnvironmentVariableName)=bash + local -x \(CompletionShell.shellVersionEnvironmentVariableName) + \(CompletionShell.shellVersionEnvironmentVariableName)="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")" + local -r \(CompletionShell.shellVersionEnvironmentVariableName) local -r cur="${2}" local -r prev="${3}" diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 30931fb0f..c0fa50fd7 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -1,9 +1,10 @@ #!/bin/bash _math() { - export SAP_SHELL=bash - SAP_SHELL_VERSION="$(IFS='.'; printf %s "${BASH_VERSINFO[*]}")" - export SAP_SHELL_VERSION + local -xr SAP_SHELL=bash + local -x SAP_SHELL_VERSION + SAP_SHELL_VERSION="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")" + local -r SAP_SHELL_VERSION local -r cur="${2}" local -r prev="${3}" diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index c7b465d80..ac2a0cf4c 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -1,9 +1,10 @@ #!/bin/bash _base_test() { - export SAP_SHELL=bash - SAP_SHELL_VERSION="$(IFS='.'; printf %s "${BASH_VERSINFO[*]}")" - export SAP_SHELL_VERSION + local -xr SAP_SHELL=bash + local -x SAP_SHELL_VERSION + SAP_SHELL_VERSION="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")" + local -r SAP_SHELL_VERSION local -r cur="${2}" local -r prev="${3}" From cf8c2790958d85fb729b97a617a26e6208eaf811 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:37:15 -0500 Subject: [PATCH 11/28] Overhaul BashCompletionsGenerator.swift as [ParsableCommand.Type] extension. Inline some single-use functions. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 135 ++++++++---------- .../Completions/CompletionsGenerator.swift | 2 +- 2 files changed, 57 insertions(+), 80 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 11a0039d1..26b02dec3 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -9,31 +9,34 @@ // //===----------------------------------------------------------------------===// -struct BashCompletionsGenerator { +extension [ParsableCommand.Type] { /// Generates a Bash completion script for the given command. - static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { + var bashCompletionScript: String { // TODO: Add a check to see if the command is installed where we expect? - """ - #!/bin/bash + // swift-format-ignore: NeverForceUnwrap + // Preconditions: + // - first must be non-empty for a bash completion script to be of use. + // - first is guaranteed non-empty in the one place where this computed var is used. + let commandName = first!._commandName + return """ + #!/bin/bash - \(generateCompletionFunction([type])) + \(completionFunctions) - complete -F \([type].completionFunctionName().shellEscapeForVariableName()) \(type._commandName) - """ + complete -F \(completionFunctionName().shellEscapeForVariableName()) \(commandName) + """ } /// Generates a Bash completion function for the last command in the given list. - private static func generateCompletionFunction( - _ commands: [ParsableCommand.Type] - ) -> String { - guard let type = commands.last else { + private var completionFunctions: String { + guard let type = last else { fatalError() } let functionName = - commands.completionFunctionName().shellEscapeForVariableName() + completionFunctionName().shellEscapeForVariableName() // The root command gets a different treatment for the parsing index. - let isRootCommand = commands.count == 1 + let isRootCommand = count == 1 let dollarOne = isRootCommand ? "1" : "${1}" let subcommandArgument = isRootCommand ? "2" : "$((${1}+1))" @@ -48,11 +51,29 @@ struct BashCompletionsGenerator { // command — these are the dash-prefixed names of options and flags as well // as all the subcommand names. let completionWords = - generateArgumentWords(commands) + subcommands.map { $0._commandName } + argumentsForHelp(visibility: .default).flatMap { $0.bashCompletionWords } + + subcommands.map { $0._commandName } // Generate additional top-level completions — these are completion lists // or custom function-based word lists from positional arguments. - let additionalCompletions = generateArgumentCompletions(commands) + let additionalCompletions = + ArgumentSet(type, visibility: .default, parent: nil) + .compactMap { arg -> String? in + guard arg.isPositional else { return nil } + + switch arg.completion.kind { + case .default, .file, .directory: + return nil + case .list(let list): + return list.joined(separator: " ") + case .shellCommand(let command): + return "$(\(command))" + case .custom: + return """ + $("${COMP_WORDS[0]}" \(arg.customCompletionCall(self)) "${COMP_WORDS[@]}") + """ + } + } // Start building the resulting function code. var result = "\(functionName)() {\n" @@ -90,7 +111,23 @@ struct BashCompletionsGenerator { // Generate the case pattern-matching statements for option values. // If there aren't any, skip the case block altogether. - let optionHandlers = generateOptionHandlers(commands) + let optionHandlers = + ArgumentSet(type, visibility: .default, parent: nil) + .compactMap { arg -> String? in + let words = arg.bashCompletionWords + if words.isEmpty { return nil } + + // Flags don't take a value, so we don't provide follow-on completions. + if arg.isNullary { return nil } + + return """ + \(arg.bashCompletionWords.joined(separator: "|"))) + \(arg.bashValueCompletion(self).indentingEachLine(by: 8))\ + return + ;; + """ + } + .joined(separator: "\n") if !optionHandlers.isEmpty { result += """ case "${prev}" in @@ -124,74 +161,14 @@ struct BashCompletionsGenerator { """ - return result - + subcommands - .map { generateCompletionFunction(commands + [$0]) } - .joined() - } - - /// Returns the option and flag names that can be top-level completions. - private static func generateArgumentWords( - _ commands: [ParsableCommand.Type] - ) -> [String] { - commands - .argumentsForHelp(visibility: .default) - .flatMap { $0.bashCompletionWords() } - } - - /// Returns additional top-level completions from positional arguments. - /// - /// These consist of completions that are defined as `.list` or `.custom`. - private static func generateArgumentCompletions( - _ commands: [ParsableCommand.Type] - ) -> [String] { - guard let type = commands.last else { return [] } - return ArgumentSet(type, visibility: .default, parent: nil) - .compactMap { arg -> String? in - guard arg.isPositional else { return nil } - - switch arg.completion.kind { - case .default, .file, .directory: - return nil - case .list(let list): - return list.joined(separator: " ") - case .shellCommand(let command): - return "$(\(command))" - case .custom: - return """ - $("${COMP_WORDS[0]}" \(arg.customCompletionCall(commands)) "${COMP_WORDS[@]}") - """ - } - } - } - - /// Returns the case-matching statements for supplying completions after an option or flag. - private static func generateOptionHandlers( - _ commands: [ParsableCommand.Type] - ) -> String { - guard let type = commands.last else { return "" } - return ArgumentSet(type, visibility: .default, parent: nil) - .compactMap { arg -> String? in - let words = arg.bashCompletionWords() - if words.isEmpty { return nil } - - // Flags don't take a value, so we don't provide follow-on completions. - if arg.isNullary { return nil } - - return """ - \(arg.bashCompletionWords().joined(separator: "|"))) - \(arg.bashValueCompletion(commands).indentingEachLine(by: 8))\ - return - ;; - """ - } - .joined(separator: "\n") + return + result + subcommands.map { (self + [$0]).completionFunctions }.joined() } } extension ArgumentDefinition { /// Returns the different completion names for this argument. - fileprivate func bashCompletionWords() -> [String] { + fileprivate var bashCompletionWords: [String] { help.visibility.base == .default ? names.map(\.synopsisString) : [] diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index f725dd9d0..609427e05 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -142,7 +142,7 @@ struct CompletionsGenerator { case .zsh: return [command].zshCompletionScript case .bash: - return BashCompletionsGenerator.generateCompletionScript(command) + return [command].bashCompletionScript case .fish: return FishCompletionsGenerator.generateCompletionScript(command) default: From 425657b1eb8fd6fe0a9bdc98cfd532d75d98f3f9 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:48:26 -0500 Subject: [PATCH 12/28] =?UTF-8?q?Move=20bashValueCompletion(=E2=80=A6)=20i?= =?UTF-8?q?n=20BashCompletionsGenerator.swift.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move from ArgumentDefinition extension to [ParsableCommand.Type] extension. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 26b02dec3..c72696137 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -122,7 +122,7 @@ extension [ParsableCommand.Type] { return """ \(arg.bashCompletionWords.joined(separator: "|"))) - \(arg.bashValueCompletion(self).indentingEachLine(by: 8))\ + \(bashValueCompletion(arg).indentingEachLine(by: 8))\ return ;; """ @@ -164,23 +164,12 @@ extension [ParsableCommand.Type] { return result + subcommands.map { (self + [$0]).completionFunctions }.joined() } -} - -extension ArgumentDefinition { - /// Returns the different completion names for this argument. - fileprivate var bashCompletionWords: [String] { - help.visibility.base == .default - ? names.map(\.synopsisString) - : [] - } - /// Returns the bash completions that can follow this argument's `--name`. + /// Returns the bash completions that can follow the given argument's `--name`. /// /// Uses bash-completion for file and directory values if available. - fileprivate func bashValueCompletion( - _ commands: [ParsableCommand.Type] - ) -> String { - switch completion.kind { + private func bashValueCompletion(_ arg: ArgumentDefinition) -> String { + switch arg.completion.kind { case .default: return "" @@ -236,9 +225,18 @@ extension ArgumentDefinition { case .custom: // Generate a call back into the command to retrieve a completions list return """ - COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" \(customCompletionCall(commands)) "${COMP_WORDS[@]}")" -- "${cur}")) + COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" \(arg.customCompletionCall(self)) "${COMP_WORDS[@]}")" -- "${cur}")) """ } } } + +extension ArgumentDefinition { + /// Returns the different completion names for this argument. + fileprivate var bashCompletionWords: [String] { + help.visibility.base == .default + ? names.map(\.synopsisString) + : [] + } +} From 42f41c1ee56616035c10b6a41b91e9ac6e8be8d7 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 20:37:48 -0500 Subject: [PATCH 13/28] Overhaul bash completion script generation: Attempt to emulate the much more complete & correct zsh completion script. Offer candidates for only the current positional / option value, not for all. Generate completions for positionals the same as for option values. Offer flags & options only if no prior option-terminator marker (a standalone --). Offer flags & options only if current word starts with a -, or if there are no remaining positional parameters. Parse options prior to subcommands later in the command line. Offer flags & options only once. Do not offer flags or options if the current word is an option value. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 304 +++++++++++++----- .../Completions/CompletionsGenerator.swift | 8 + .../testMathBashCompletionScript().bash | 274 +++++++++++----- .../Snapshots/testBase_Bash().bash | 208 +++++++++--- 4 files changed, 599 insertions(+), 195 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index c72696137..ea05b9fed 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -21,8 +21,122 @@ extension [ParsableCommand.Type] { return """ #!/bin/bash - \(completionFunctions) + # positional arguments: + # + # - 1: the current (sub)command's count of positional arguments + # + # required variables: + # + # - flags: the flags that the current (sub)command can accept + # - options: the 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 + # - positional_number: set to the current positional number + # - unparsed_words: remove all flags, options, and option values for this (sub)command + \(offerFlagsOptionsFunctionName)() { + local -ir positional_count="${1}" + positional_number=0 + + local was_flag_option_terminator_seen=false + local is_parsing_option_value=false + + local -ar unparsed_word_indices=("${!unparsed_words[@]}") + local -i word_index + for word_index in "${unparsed_word_indices[@]}"; do + if "${is_parsing_option_value}"; then + # This word is an option value: + # Reset marker for next word iff not currently the last word + [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]] && is_parsing_option_value=false + unset "unparsed_words[${word_index}]" + # Do not process this word as a flag or an option + continue + fi + + local word="${unparsed_words["${word_index}"]}" + if ! "${was_flag_option_terminator_seen}"; then + case "${word}" in + --) + unset "unparsed_words[${word_index}]" + # by itself -- is a flag/option terminator, but if it is the last word, it is the start of a completion + if [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]]; then + was_flag_option_terminator_seen=true + fi + continue + ;; + -*) + # ${word} is a flag or an option + # If ${word} is an option, mark that the next word to be parsed is an option value + # TODO: handle joined-value options (-o=file.ext), stacked flags (-aBc), legacy long (-long), combos + # TODO: if multi-valued options can exist, support them + local option + for option in "${options[@]}"; do + [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break + done + + # Remove ${word} from ${flags} or ${options} so it isn't offered again + # TODO: handle repeatable flags & options + # TODO: remove equivalent options (-h/--help) & exclusive options (--yes/--no) + local not_found=true + local -i index + for index in "${!flags[@]}"; do + if [[ "${flags[${index}]}" = "${word}" ]]; then + unset "flags[${index}]" + flags=("${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[@]}") + break + fi + done + fi + unset "unparsed_words[${word_index}]" + continue + ;; + esac + fi + + # ${word} is neither a flag, nor an option, nor an option value + # TODO: can SAP be configured to require options before positionals? + if [[ "${positional_number}" -lt "${positional_count}" ]]; then + # ${word} is a positional + ((positional_number++)) + unset "unparsed_words[${word_index}]" + else + if [[ -z "${word}" ]]; then + # Could be completing a flag, option, or subcommand + positional_number=-1 + else + # ${word} is a subcommand or invalid, so stop processing this (sub)command + positional_number=-2 + fi + break + fi + done + + unparsed_words=("${unparsed_words[@]}") + + # TODO: offer flags & options after all positionals iff they're allowed after positionals + if\\ + ! "${was_flag_option_terminator_seen}"\\ + && ! "${is_parsing_option_value}"\\ + && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] + then + COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) + fi + } + \(completionFunctions)\ complete -F \(completionFunctionName().shellEscapeForVariableName()) \(commandName) """ } @@ -35,52 +149,19 @@ extension [ParsableCommand.Type] { let functionName = completionFunctionName().shellEscapeForVariableName() - // The root command gets a different treatment for the parsing index. - let isRootCommand = count == 1 - let dollarOne = isRootCommand ? "1" : "${1}" - let subcommandArgument = isRootCommand ? "2" : "$((${1}+1))" - - // Include 'help' in the list of subcommands for the root command. - var subcommands = type.configuration.subcommands - .filter { $0.configuration.shouldDisplay } - if !subcommands.isEmpty && isRootCommand { - subcommands.append(HelpCommand.self) - } + var subcommands = + type.configuration.subcommands.filter { $0.configuration.shouldDisplay } - // Generate the words that are available at the "top level" of this - // command — these are the dash-prefixed names of options and flags as well - // as all the subcommand names. - let completionWords = - argumentsForHelp(visibility: .default).flatMap { $0.bashCompletionWords } - + subcommands.map { $0._commandName } + // Start building the resulting function code. + var result = "" - // Generate additional top-level completions — these are completion lists - // or custom function-based word lists from positional arguments. - let additionalCompletions = - ArgumentSet(type, visibility: .default, parent: nil) - .compactMap { arg -> String? in - guard arg.isPositional else { return nil } - - switch arg.completion.kind { - case .default, .file, .directory: - return nil - case .list(let list): - return list.joined(separator: " ") - case .shellCommand(let command): - return "$(\(command))" - case .custom: - return """ - $("${COMP_WORDS[0]}" \(arg.customCompletionCall(self)) "${COMP_WORDS[@]}") - """ - } + // Include initial setup iff the root command. + let declareTopLevelArray: String + if count == 1 { + if !subcommands.isEmpty { + subcommands.append(HelpCommand.self) } - // Start building the resulting function code. - var result = "\(functionName)() {\n" - - // The function that represents the root command has some additional setup - // that other command functions don't need. - if isRootCommand { result += """ local -xr \(CompletionShell.shellEnvironmentVariableName)=bash local -x \(CompletionShell.shellVersionEnvironmentVariableName) @@ -90,24 +171,29 @@ extension [ParsableCommand.Type] { local -r cur="${2}" local -r prev="${3}" + local -i positional_number + local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}") + """ - } - // Start by declaring a local var for the top-level completions. - // Return immediately if the completion matching hasn't moved further. - result += " opts=\"\(completionWords.joined(separator: " "))\"\n" - for line in additionalCompletions { - result += " opts=\"${opts} \(line)\"\n" + declareTopLevelArray = "local -a " + } else { + declareTopLevelArray = "" } - result += """ - if [[ "${COMP_CWORD}" == "\(dollarOne)" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi + let positionalArguments = positionalArguments - """ + let flags = flags + let options = options + if !flags.isEmpty || !options.isEmpty { + result += """ + \(declareTopLevelArray)flags=(\(flags.joined(separator: " "))) + \(declareTopLevelArray)options=(\(options.joined(separator: " "))) + \(offerFlagsOptionsFunctionName) \(positionalArguments.count) + + """ + } // Generate the case pattern-matching statements for option values. // If there aren't any, skip the case block altogether. @@ -130,6 +216,9 @@ extension [ParsableCommand.Type] { .joined(separator: "\n") if !optionHandlers.isEmpty { result += """ + + # Offer option value completions + # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in \(optionHandlers) esac @@ -137,32 +226,91 @@ extension [ParsableCommand.Type] { """ } - // Build out completions for the subcommands. + if !positionalArguments.allSatisfy({ bashValueCompletion($0).isEmpty }) { + var position = 0 + result += """ + + # Offer positional completions + case "${positional_number}" in + \(positionalArguments + .compactMap { arg in + position += 1 + let completion = bashValueCompletion(arg) + return completion.isEmpty + ? nil + : """ + \(position)) + \(completion.indentingEachLine(by: 8))\ + return + ;; + + """ + } + .joined() + )\ + esac + + """ + } + if !subcommands.isEmpty { - // Subcommands have their own case statement that delegates out to - // the subcommand completion functions. - result += " case \"${COMP_WORDS[\(dollarOne)]}\" in\n" - for subcommand in subcommands { - result += """ - \(subcommand._commandName)) - \(functionName)_\(subcommand._commandName) \(subcommandArgument) - return - ;; + result += """ - """ - } - result += " esac\n" + # Offer subcommand / subcommand argument completions + local -r subcommand="${unparsed_words[0]}" + unset 'unparsed_words[0]' + unparsed_words=("${unparsed_words[@]}") + case "${subcommand}" in + \(subcommands.map { $0._commandName }.joined(separator: "|"))) + # Offer subcommand argument completions + "\(functionName)_${subcommand}" + ;; + *) + # Offer subcommand completions + COMPREPLY+=($(compgen -W '\( + subcommands.map { $0._commandName.shellEscapeForSingleQuotedString() }.joined(separator: " ") + )' -- "${cur}")) + ;; + esac + + """ + } + + if result.isEmpty { + result = " :\n" } - // Finish off the function. - result += """ - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + return """ + \(functionName)() { + \(result)\ } + \(subcommands.map { (self + [$0]).completionFunctions }.joined()) """ + } - return - result + subcommands.map { (self + [$0]).completionFunctions }.joined() + /// Returns flags for the last command of the given array. + private var flags: [String] { + argumentsForHelp(visibility: .default).flatMap { + switch ($0.kind, $0.update) { + case (.named, .nullary): + return $0.bashCompletionWords + default: + return [] + } + } + } + + /// Returns options for the last command of the given array. + private var options: [String] { + argumentsForHelp(visibility: .default).flatMap { + switch ($0.kind, $0.update) { + case (.named, .unary): + return $0.bashCompletionWords + default: + return [] + } + } } /// Returns the bash completions that can follow the given argument's `--name`. @@ -212,24 +360,28 @@ extension [ParsableCommand.Type] { case .list(let list): return """ - COMPREPLY=($(compgen -W "\(list.joined(separator: " "))" -- "${cur}")) + COMPREPLY+=($(compgen -W "\(list.joined(separator: " "))" -- "${cur}")) """ case .shellCommand(let command): return """ - COMPREPLY=($(\(command))) + COMPREPLY+=($(\(command))) """ case .custom: // Generate a call back into the command to retrieve a completions list return """ - COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" \(arg.customCompletionCall(self)) "${COMP_WORDS[@]}")" -- "${cur}")) + COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" \(arg.customCompletionCall(self)) "${COMP_WORDS[@]}")" -- "${cur}")) """ } } + + private var offerFlagsOptionsFunctionName: String { + "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_offer_flags_options" + } } extension ArgumentDefinition { diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 609427e05..ac0edc254 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -176,6 +176,14 @@ extension ParsableCommand { } extension [ParsableCommand.Type] { + var positionalArguments: [ArgumentDefinition] { + guard let command = last else { + return [] + } + return ArgumentSet(command, visibility: .default, parent: nil) + .filter(\.isPositional) + } + /// Include default 'help' subcommand in nonempty subcommand list if & only if /// no help subcommand already exists. mutating func addHelpSubcommandIfMissing() { diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index c0fa50fd7..0f106ae77 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -1,5 +1,120 @@ #!/bin/bash +# positional arguments: +# +# - 1: the current (sub)command's count of positional arguments +# +# required variables: +# +# - flags: the flags that the current (sub)command can accept +# - options: the 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 +# - 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() { + local -ir positional_count="${1}" + positional_number=0 + + local was_flag_option_terminator_seen=false + local is_parsing_option_value=false + + local -ar unparsed_word_indices=("${!unparsed_words[@]}") + local -i word_index + for word_index in "${unparsed_word_indices[@]}"; do + if "${is_parsing_option_value}"; then + # This word is an option value: + # Reset marker for next word iff not currently the last word + [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]] && is_parsing_option_value=false + unset "unparsed_words[${word_index}]" + # Do not process this word as a flag or an option + continue + fi + + local word="${unparsed_words["${word_index}"]}" + if ! "${was_flag_option_terminator_seen}"; then + case "${word}" in + --) + unset "unparsed_words[${word_index}]" + # by itself -- is a flag/option terminator, but if it is the last word, it is the start of a completion + if [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]]; then + was_flag_option_terminator_seen=true + fi + continue + ;; + -*) + # ${word} is a flag or an option + # If ${word} is an option, mark that the next word to be parsed is an option value + # TODO: handle joined-value options (-o=file.ext), stacked flags (-aBc), legacy long (-long), combos + # TODO: if multi-valued options can exist, support them + local option + for option in "${options[@]}"; do + [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break + done + + # Remove ${word} from ${flags} or ${options} so it isn't offered again + # TODO: handle repeatable flags & options + # TODO: remove equivalent options (-h/--help) & exclusive options (--yes/--no) + local not_found=true + local -i index + for index in "${!flags[@]}"; do + if [[ "${flags[${index}]}" = "${word}" ]]; then + unset "flags[${index}]" + flags=("${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[@]}") + break + fi + done + fi + unset "unparsed_words[${word_index}]" + continue + ;; + esac + fi + + # ${word} is neither a flag, nor an option, nor an option value + # TODO: can SAP be configured to require options before positionals? + if [[ "${positional_number}" -lt "${positional_count}" ]]; then + # ${word} is a positional + ((positional_number++)) + unset "unparsed_words[${word_index}]" + else + if [[ -z "${word}" ]]; then + # Could be completing a flag, option, or subcommand + positional_number=-1 + else + # ${word} is a subcommand or invalid, so stop processing this (sub)command + positional_number=-2 + fi + break + fi + done + + unparsed_words=("${unparsed_words[@]}") + + # TODO: offer flags & options after all positionals iff they're allowed after positionals + if\ + ! "${was_flag_option_terminator_seen}"\ + && ! "${is_parsing_option_value}"\ + && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] + then + COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) + fi +} + _math() { local -xr SAP_SHELL=bash local -x SAP_SHELL_VERSION @@ -9,99 +124,90 @@ _math() { local -r cur="${2}" local -r prev="${3}" - opts="--version -h --help add multiply stats help" - if [[ "${COMP_CWORD}" == "1" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi - case "${COMP_WORDS[1]}" in - add) - _math_add 2 - return - ;; - multiply) - _math_multiply 2 - return + local -i positional_number + local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}") + + local -a flags=(--version -h --help) + local -a options=() + __math_offer_flags_options 0 + + # Offer subcommand / subcommand argument completions + local -r subcommand="${unparsed_words[0]}" + unset 'unparsed_words[0]' + unparsed_words=("${unparsed_words[@]}") + case "${subcommand}" in + add|multiply|stats|help) + # Offer subcommand argument completions + "_math_${subcommand}" ;; - stats) - _math_stats 2 - return - ;; - help) - _math_help 2 - return + *) + # Offer subcommand completions + COMPREPLY+=($(compgen -W 'add multiply stats help' -- "${cur}")) ;; esac - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } + _math_add() { - opts="--hex-output -x --version -h --help" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + flags=(--hex-output -x --version -h --help) + options=() + __math_offer_flags_options 1 } + _math_multiply() { - opts="--hex-output -x --version -h --help" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + flags=(--hex-output -x --version -h --help) + options=() + __math_offer_flags_options 1 } + _math_stats() { - opts="--version -h --help average stdev quantiles" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi - case "${COMP_WORDS[${1}]}" in - average) - _math_stats_average $((${1}+1)) - return - ;; - stdev) - _math_stats_stdev $((${1}+1)) - return + flags=(--version -h --help) + options=() + __math_offer_flags_options 0 + + # Offer subcommand / subcommand argument completions + local -r subcommand="${unparsed_words[0]}" + unset 'unparsed_words[0]' + unparsed_words=("${unparsed_words[@]}") + case "${subcommand}" in + average|stdev|quantiles) + # Offer subcommand argument completions + "_math_stats_${subcommand}" ;; - quantiles) - _math_stats_quantiles $((${1}+1)) - return + *) + # Offer subcommand completions + COMPREPLY+=($(compgen -W 'average stdev quantiles' -- "${cur}")) ;; esac - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } + _math_stats_average() { - opts="--kind --version -h --help" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi + flags=(--version -h --help) + options=(--kind) + __math_offer_flags_options 1 + + # Offer option value completions + # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --kind) - COMPREPLY=($(compgen -W "mean median mode" -- "${cur}")) + COMPREPLY+=($(compgen -W "mean median mode" -- "${cur}")) return ;; esac - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } + _math_stats_stdev() { - opts="--version -h --help" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + flags=(--version -h --help) + options=() + __math_offer_flags_options 1 } + _math_stats_quantiles() { - opts="--file --directory --shell --custom --version -h --help" - opts="${opts} alphabet alligator branch braggart" - opts="${opts} $("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi + flags=(--version -h --help) + options=(--file --directory --shell --custom) + __math_offer_flags_options 3 + + # Offer option value completions + # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --file) if declare -F _filedir >/dev/null; then @@ -130,24 +236,32 @@ _math_stats_quantiles() { return ;; --shell) - COMPREPLY=($(head -100 /usr/share/dict/words | tail -50)) + COMPREPLY+=($(head -100 /usr/share/dict/words | tail -50)) return ;; --custom) - COMPREPLY=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "${cur}")) + COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "${cur}")) return ;; esac - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) -} -_math_help() { - opts="--version" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + + # Offer positional completions + case "${positional_number}" in + 1) + COMPREPLY+=($(compgen -W "alphabet alligator branch braggart" -- "${cur}")) return - fi - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + ;; + 2) + COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" -- "${cur}")) + return + ;; + esac } +_math_help() { + flags=(--version) + options=() + __math_offer_flags_options 1 +} complete -F _math math diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index ac2a0cf4c..a210c9908 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -1,5 +1,120 @@ #!/bin/bash +# positional arguments: +# +# - 1: the current (sub)command's count of positional arguments +# +# required variables: +# +# - flags: the flags that the current (sub)command can accept +# - options: the 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 +# - 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() { + local -ir positional_count="${1}" + positional_number=0 + + local was_flag_option_terminator_seen=false + local is_parsing_option_value=false + + local -ar unparsed_word_indices=("${!unparsed_words[@]}") + local -i word_index + for word_index in "${unparsed_word_indices[@]}"; do + if "${is_parsing_option_value}"; then + # This word is an option value: + # Reset marker for next word iff not currently the last word + [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]] && is_parsing_option_value=false + unset "unparsed_words[${word_index}]" + # Do not process this word as a flag or an option + continue + fi + + local word="${unparsed_words["${word_index}"]}" + if ! "${was_flag_option_terminator_seen}"; then + case "${word}" in + --) + unset "unparsed_words[${word_index}]" + # by itself -- is a flag/option terminator, but if it is the last word, it is the start of a completion + if [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]]; then + was_flag_option_terminator_seen=true + fi + continue + ;; + -*) + # ${word} is a flag or an option + # If ${word} is an option, mark that the next word to be parsed is an option value + # TODO: handle joined-value options (-o=file.ext), stacked flags (-aBc), legacy long (-long), combos + # TODO: if multi-valued options can exist, support them + local option + for option in "${options[@]}"; do + [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break + done + + # Remove ${word} from ${flags} or ${options} so it isn't offered again + # TODO: handle repeatable flags & options + # TODO: remove equivalent options (-h/--help) & exclusive options (--yes/--no) + local not_found=true + local -i index + for index in "${!flags[@]}"; do + if [[ "${flags[${index}]}" = "${word}" ]]; then + unset "flags[${index}]" + flags=("${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[@]}") + break + fi + done + fi + unset "unparsed_words[${word_index}]" + continue + ;; + esac + fi + + # ${word} is neither a flag, nor an option, nor an option value + # TODO: can SAP be configured to require options before positionals? + if [[ "${positional_number}" -lt "${positional_count}" ]]; then + # ${word} is a positional + ((positional_number++)) + unset "unparsed_words[${word_index}]" + else + if [[ -z "${word}" ]]; then + # Could be completing a flag, option, or subcommand + positional_number=-1 + else + # ${word} is a subcommand or invalid, so stop processing this (sub)command + positional_number=-2 + fi + break + fi + done + + unparsed_words=("${unparsed_words[@]}") + + # TODO: offer flags & options after all positionals iff they're allowed after positionals + if\ + ! "${was_flag_option_terminator_seen}"\ + && ! "${is_parsing_option_value}"\ + && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]] + then + COMPREPLY+=($(compgen -W "${flags[*]} ${options[*]}" -- "${cur}")) + fi +} + _base_test() { local -xr SAP_SHELL=bash local -x SAP_SHELL_VERSION @@ -9,23 +124,25 @@ _base_test() { local -r cur="${2}" local -r prev="${3}" - opts="--name --kind --other-kind --path1 --path2 --path3 --one --two --three --kind-counter --rep1 -r --rep2 -h --help sub-command escaped-command help" - opts="${opts} $("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" - opts="${opts} $("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" - if [[ "${COMP_CWORD}" == "1" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi + local -i positional_number + local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}") + + local -a flags=(--one --two --three --kind-counter -h --help) + local -a options=(--name --kind --other-kind --path1 --path2 --path3 --rep1 -r --rep2) + __base_test_offer_flags_options 2 + + # Offer option value completions + # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --name) return ;; --kind) - COMPREPLY=($(compgen -W "one two custom-three" -- "${cur}")) + COMPREPLY+=($(compgen -W "one two custom-three" -- "${cur}")) return ;; --other-kind) - COMPREPLY=($(compgen -W "b1_bash b2_bash b3_bash" -- "${cur}")) + COMPREPLY+=($(compgen -W "b1_bash b2_bash b3_bash" -- "${cur}")) return ;; --path1) @@ -45,7 +162,7 @@ _base_test() { return ;; --path3) - COMPREPLY=($(compgen -W "c1_bash c2_bash c3_bash" -- "${cur}")) + COMPREPLY+=($(compgen -W "c1_bash c2_bash c3_bash" -- "${cur}")) return ;; --rep1) @@ -55,52 +172,65 @@ _base_test() { return ;; esac - case "${COMP_WORDS[1]}" in - sub-command) - _base_test_sub-command 2 + + # Offer positional completions + case "${positional_number}" in + 1) + COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" -- "${cur}")) return ;; - escaped-command) - _base_test_escaped-command 2 + 2) + COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" -- "${cur}")) return ;; - help) - _base_test_help 2 - return + esac + + # Offer subcommand / subcommand argument completions + local -r subcommand="${unparsed_words[0]}" + unset 'unparsed_words[0]' + unparsed_words=("${unparsed_words[@]}") + case "${subcommand}" in + sub-command|escaped-command|help) + # Offer subcommand argument completions + "_base_test_${subcommand}" + ;; + *) + # Offer subcommand completions + COMPREPLY+=($(compgen -W 'sub-command escaped-command help' -- "${cur}")) ;; esac - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) } + _base_test_sub_command() { - opts="-h --help" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + flags=(-h --help) + options=() + __base_test_offer_flags_options 0 } + _base_test_escaped_command() { - opts="--one -h --help" - opts="${opts} $("${COMP_WORDS[0]}" ---completion escaped-command -- two "${COMP_WORDS[@]}")" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) - return - fi + flags=(-h --help) + options=(--one) + __base_test_offer_flags_options 1 + + # Offer option value completions + # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --one) return ;; esac - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) -} -_base_test_help() { - opts="" - if [[ "${COMP_CWORD}" == "${1}" ]]; then - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + + # Offer positional completions + case "${positional_number}" in + 1) + COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion escaped-command -- two "${COMP_WORDS[@]}")" -- "${cur}")) return - fi - COMPREPLY=($(compgen -W "${opts}" -- "${cur}")) + ;; + esac } +_base_test_help() { + : +} complete -F _base_test base-test \ No newline at end of file From 48303172f88eccfc1938b16f93b9a055217e666e Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Mon, 27 Jan 2025 09:57:19 -0500 Subject: [PATCH 14/28] Add default help to bash completions iff no existing help subcommand. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/BashCompletionsGenerator.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index ea05b9fed..368d7483e 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -158,9 +158,7 @@ extension [ParsableCommand.Type] { // Include initial setup iff the root command. let declareTopLevelArray: String if count == 1 { - if !subcommands.isEmpty { - subcommands.append(HelpCommand.self) - } + subcommands.addHelpSubcommandIfMissing() result += """ local -xr \(CompletionShell.shellEnvironmentVariableName)=bash From 9252f0fc6f1be4d117f023fc222ff150597f10fb Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 12:29:14 -0500 Subject: [PATCH 15/28] Improve bash file & directory completions. Do not split paths with spaces into separate completions. Escape spaces in paths. Append / to directory paths. Do not use _filedir from bash-completions; use builtin bash constructs instead. Fix broken existing escaping of single quotes in file extension filters. More succinct & performant. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 47 +++++++++---------- .../testMathBashCompletionScript().bash | 35 ++++++-------- .../Snapshots/testBase_Bash().bash | 25 +++++----- 3 files changed, 50 insertions(+), 57 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 368d7483e..2f56e01e4 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -136,8 +136,15 @@ extension [ParsableCommand.Type] { fi } + \(addCompletionsFunctionName)() { + local completion + while IFS='' read -r completion; do + COMPREPLY+=("${completion}") + done < <(IFS=$'\\n' compgen "${@}" -- "${cur}") + } + \(completionFunctions)\ - complete -F \(completionFunctionName().shellEscapeForVariableName()) \(commandName) + complete -o filenames -F \(completionFunctionName().shellEscapeForVariableName()) \(commandName) """ } @@ -161,6 +168,10 @@ extension [ParsableCommand.Type] { subcommands.addHelpSubcommandIfMissing() result += """ + trap "$(shopt -p);$(shopt -po)" RETURN + shopt -s extglob + set +o posix + local -xr \(CompletionShell.shellEnvironmentVariableName)=bash local -x \(CompletionShell.shellVersionEnvironmentVariableName) \(CompletionShell.shellVersionEnvironmentVariableName)="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")" @@ -312,8 +323,6 @@ extension [ParsableCommand.Type] { } /// Returns the bash completions that can follow the given argument's `--name`. - /// - /// Uses bash-completion for file and directory values if available. private func bashValueCompletion(_ arg: ArgumentDefinition) -> String { switch arg.completion.kind { case .default: @@ -321,38 +330,22 @@ extension [ParsableCommand.Type] { case .file(let extensions) where extensions.isEmpty: return """ - if declare -F _filedir >/dev/null; then - _filedir - else - COMPREPLY=($(compgen -f -- "${cur}")) - fi + \(addCompletionsFunctionName) -f """ case .file(let extensions): - var safeExts = extensions.map { $0.shellEscapeForSingleQuotedString() } - safeExts.append(contentsOf: safeExts.map { $0.uppercased() }) - + let exts = extensions.map { $0.shellEscapeForSingleQuotedString() } + .flatMap { [$0, $0.uppercased()] } + .joined(separator: "|") return """ - if declare -F _filedir >/dev/null; then - \(safeExts.map { "_filedir '\($0)'" }.joined(separator:"\n ")) - _filedir -d - else - COMPREPLY=( - \(safeExts.map { "$(compgen -f -X '!*.\($0)' -- \"${cur}\")" }.joined(separator: "\n ")) - $(compgen -d -- "${cur}") - ) - fi + \(addCompletionsFunctionName) -o plusdirs -fX '!*.@(\(exts))' """ case .directory: return """ - if declare -F _filedir >/dev/null; then - _filedir -d - else - COMPREPLY=($(compgen -d -- "${cur}")) - fi + \(addCompletionsFunctionName) -d """ @@ -380,6 +373,10 @@ extension [ParsableCommand.Type] { private var offerFlagsOptionsFunctionName: String { "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_offer_flags_options" } + + private var addCompletionsFunctionName: String { + "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_add_completions" + } } extension ArgumentDefinition { diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 0f106ae77..a3e25bda4 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -115,7 +115,18 @@ __math_offer_flags_options() { fi } +__math_add_completions() { + local completion + while IFS='' read -r completion; do + COMPREPLY+=("${completion}") + done < <(IFS=$'\n' compgen "${@}" -- "${cur}") +} + _math() { + trap "$(shopt -p);$(shopt -po)" RETURN + shopt -s extglob + set +o posix + local -xr SAP_SHELL=bash local -x SAP_SHELL_VERSION SAP_SHELL_VERSION="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")" @@ -210,29 +221,11 @@ _math_stats_quantiles() { # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --file) - if declare -F _filedir >/dev/null; then - _filedir 'txt' - _filedir 'md' - _filedir 'TXT' - _filedir 'MD' - _filedir -d - else - COMPREPLY=( - $(compgen -f -X '!*.txt' -- "${cur}") - $(compgen -f -X '!*.md' -- "${cur}") - $(compgen -f -X '!*.TXT' -- "${cur}") - $(compgen -f -X '!*.MD' -- "${cur}") - $(compgen -d -- "${cur}") - ) - fi + __math_add_completions -o plusdirs -fX '!*.@(txt|TXT|md|MD)' return ;; --directory) - if declare -F _filedir >/dev/null; then - _filedir -d - else - COMPREPLY=($(compgen -d -- "${cur}")) - fi + __math_add_completions -d return ;; --shell) @@ -264,4 +257,4 @@ _math_help() { __math_offer_flags_options 1 } -complete -F _math math +complete -o filenames -F _math math diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index a210c9908..845e50352 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -115,7 +115,18 @@ __base_test_offer_flags_options() { fi } +__base_test_add_completions() { + local completion + while IFS='' read -r completion; do + COMPREPLY+=("${completion}") + done < <(IFS=$'\n' compgen "${@}" -- "${cur}") +} + _base_test() { + trap "$(shopt -p);$(shopt -po)" RETURN + shopt -s extglob + set +o posix + local -xr SAP_SHELL=bash local -x SAP_SHELL_VERSION SAP_SHELL_VERSION="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")" @@ -146,19 +157,11 @@ _base_test() { return ;; --path1) - if declare -F _filedir >/dev/null; then - _filedir - else - COMPREPLY=($(compgen -f -- "${cur}")) - fi + __base_test_add_completions -f return ;; --path2) - if declare -F _filedir >/dev/null; then - _filedir - else - COMPREPLY=($(compgen -f -- "${cur}")) - fi + __base_test_add_completions -f return ;; --path3) @@ -233,4 +236,4 @@ _base_test_help() { : } -complete -F _base_test base-test \ No newline at end of file +complete -o filenames -F _base_test base-test \ No newline at end of file From 1767176141de5d51249cffccb955e4aa3120b54f Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:13:20 -0500 Subject: [PATCH 16/28] Disable history ! in bash completion scripts. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/BashCompletionsGenerator.swift | 2 +- .../Snapshots/testMathBashCompletionScript().bash | 2 +- Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 2f56e01e4..802a1ad87 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -170,7 +170,7 @@ extension [ParsableCommand.Type] { result += """ trap "$(shopt -p);$(shopt -po)" RETURN shopt -s extglob - set +o posix + set +o history +o posix local -xr \(CompletionShell.shellEnvironmentVariableName)=bash local -x \(CompletionShell.shellVersionEnvironmentVariableName) diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index a3e25bda4..ec35a5150 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -125,7 +125,7 @@ __math_add_completions() { _math() { trap "$(shopt -p);$(shopt -po)" RETURN shopt -s extglob - set +o posix + set +o history +o posix local -xr SAP_SHELL=bash local -x SAP_SHELL_VERSION diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 845e50352..e2fa9c4bd 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -125,7 +125,7 @@ __base_test_add_completions() { _base_test() { trap "$(shopt -p);$(shopt -po)" RETURN shopt -s extglob - set +o posix + set +o history +o posix local -xr SAP_SHELL=bash local -x SAP_SHELL_VERSION From 6268c97b5caabcf7279577db85947fc551e0083f Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 21:23:46 -0500 Subject: [PATCH 17/28] Do not include uppercased extensions in bash file(extensions:) completions. That behavior was a bug, not a feature. Released Swift Argument Parser documentation says: "Complete file names with the specified extensions." It does not mention including uppercase versions of the extensions. None of the other shells include uppercase versions of extensions. Why should uppercase versions be special? Why not case-insensitive matching? Why not lowercase versions? Etc. Any config depending on this behavior won't match uppercase extensions without manually being reconfigured, but there are a ton of other bug fixes that can also break compatibility. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 6 +++--- .../Snapshots/testMathBashCompletionScript().bash | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 802a1ad87..7ecc7e641 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -335,9 +335,9 @@ extension [ParsableCommand.Type] { """ case .file(let extensions): - let exts = extensions.map { $0.shellEscapeForSingleQuotedString() } - .flatMap { [$0, $0.uppercased()] } - .joined(separator: "|") + let exts = + extensions + .map { $0.shellEscapeForSingleQuotedString() }.joined(separator: "|") return """ \(addCompletionsFunctionName) -o plusdirs -fX '!*.@(\(exts))' diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index ec35a5150..027a530e4 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -221,7 +221,7 @@ _math_stats_quantiles() { # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --file) - __math_add_completions -o plusdirs -fX '!*.@(txt|TXT|md|MD)' + __math_add_completions -o plusdirs -fX '!*.@(txt|md)' return ;; --directory) From 5370d188621402078d7e9628dac321d7ae49b677 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 22:22:28 -0500 Subject: [PATCH 18/28] Use single quoted string for bash list completions. bash scripts now escape single quotes in list values. Any existing list values with escapes that worked in double quotes will not work in the single quotes. But now: - list values needn't be escaped - unescaped double quotes in list values won't break the script - $ & other characters won't interact with the shell, so, e.g., command substitutions cannot cause problems Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 4 +++- .../Snapshots/testMathBashCompletionScript().bash | 4 ++-- .../ArgumentParserUnitTests/Snapshots/testBase_Bash().bash | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 7ecc7e641..89ef5321f 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -351,7 +351,9 @@ extension [ParsableCommand.Type] { case .list(let list): return """ - COMPREPLY+=($(compgen -W "\(list.joined(separator: " "))" -- "${cur}")) + COMPREPLY+=($(compgen -W '\( + list.map { $0.shellEscapeForSingleQuotedString() }.joined(separator: " ") + )' -- "${cur}")) """ diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 027a530e4..31f2ff92b 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -200,7 +200,7 @@ _math_stats_average() { # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --kind) - COMPREPLY+=($(compgen -W "mean median mode" -- "${cur}")) + COMPREPLY+=($(compgen -W 'mean median mode' -- "${cur}")) return ;; esac @@ -241,7 +241,7 @@ _math_stats_quantiles() { # Offer positional completions case "${positional_number}" in 1) - COMPREPLY+=($(compgen -W "alphabet alligator branch braggart" -- "${cur}")) + COMPREPLY+=($(compgen -W 'alphabet alligator branch braggart' -- "${cur}")) return ;; 2) diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index e2fa9c4bd..8ec0ee169 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -149,11 +149,11 @@ _base_test() { return ;; --kind) - COMPREPLY+=($(compgen -W "one two custom-three" -- "${cur}")) + COMPREPLY+=($(compgen -W 'one two custom-three' -- "${cur}")) return ;; --other-kind) - COMPREPLY+=($(compgen -W "b1_bash b2_bash b3_bash" -- "${cur}")) + COMPREPLY+=($(compgen -W 'b1_bash b2_bash b3_bash' -- "${cur}")) return ;; --path1) @@ -165,7 +165,7 @@ _base_test() { return ;; --path3) - COMPREPLY+=($(compgen -W "c1_bash c2_bash c3_bash" -- "${cur}")) + COMPREPLY+=($(compgen -W 'c1_bash c2_bash c3_bash' -- "${cur}")) return ;; --rep1) From 950d2bc690c87535c2838611f057de061a07b66c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 19 Jan 2025 08:32:27 -0500 Subject: [PATCH 19/28] Allow bash list completions to contain spaces. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 5 ++--- .../Snapshots/testMathBashCompletionScript().bash | 4 ++-- .../ArgumentParserUnitTests/Snapshots/testBase_Bash().bash | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 89ef5321f..9c1f066ad 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -351,9 +351,8 @@ extension [ParsableCommand.Type] { case .list(let list): return """ - COMPREPLY+=($(compgen -W '\( - list.map { $0.shellEscapeForSingleQuotedString() }.joined(separator: " ") - )' -- "${cur}")) + \(addCompletionsFunctionName) -W\ + '\(list.map { $0.shellEscapeForSingleQuotedString() }.joined(separator: "'$'\\n''"))' """ diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 31f2ff92b..eb8a7341f 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -200,7 +200,7 @@ _math_stats_average() { # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --kind) - COMPREPLY+=($(compgen -W 'mean median mode' -- "${cur}")) + __math_add_completions -W 'mean'$'\n''median'$'\n''mode' return ;; esac @@ -241,7 +241,7 @@ _math_stats_quantiles() { # Offer positional completions case "${positional_number}" in 1) - COMPREPLY+=($(compgen -W 'alphabet alligator branch braggart' -- "${cur}")) + __math_add_completions -W 'alphabet'$'\n''alligator'$'\n''branch'$'\n''braggart' return ;; 2) diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 8ec0ee169..b1c5fc0f5 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -149,11 +149,11 @@ _base_test() { return ;; --kind) - COMPREPLY+=($(compgen -W 'one two custom-three' -- "${cur}")) + __base_test_add_completions -W 'one'$'\n''two'$'\n''custom-three' return ;; --other-kind) - COMPREPLY+=($(compgen -W 'b1_bash b2_bash b3_bash' -- "${cur}")) + __base_test_add_completions -W 'b1_bash'$'\n''b2_bash'$'\n''b3_bash' return ;; --path1) @@ -165,7 +165,7 @@ _base_test() { return ;; --path3) - COMPREPLY+=($(compgen -W 'c1_bash c2_bash c3_bash' -- "${cur}")) + __base_test_add_completions -W 'c1_bash'$'\n''c2_bash'$'\n''c3_bash' return ;; --rep1) From cff699807f9a7a65c86461b10ca3ffbc2366078c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 19 Jan 2025 10:17:50 -0500 Subject: [PATCH 20/28] Allow bash custom completions to contain spaces. Properly refuses to complete when there are no completion candidates. Still cannot complete to an empty string. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 3 ++- .../Snapshots/testMathBashCompletionScript().bash | 4 ++-- .../ArgumentParserUnitTests/Snapshots/testBase_Bash().bash | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 9c1f066ad..57e1d78c8 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -365,7 +365,8 @@ extension [ParsableCommand.Type] { case .custom: // Generate a call back into the command to retrieve a completions list return """ - COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" \(arg.customCompletionCall(self)) "${COMP_WORDS[@]}")" -- "${cur}")) + \(addCompletionsFunctionName) -W\ + "$("${COMP_WORDS[0]}" \(arg.customCompletionCall(self)) "${COMP_WORDS[@]}")" """ } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index eb8a7341f..dd05504a9 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -233,7 +233,7 @@ _math_stats_quantiles() { return ;; --custom) - COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" -- "${cur}")) + __math_add_completions -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" return ;; esac @@ -245,7 +245,7 @@ _math_stats_quantiles() { return ;; 2) - COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" -- "${cur}")) + __math_add_completions -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" return ;; esac diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index b1c5fc0f5..eaabd4cc3 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -179,11 +179,11 @@ _base_test() { # Offer positional completions case "${positional_number}" in 1) - COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" -- "${cur}")) + __base_test_add_completions -W "$("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" return ;; 2) - COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" -- "${cur}")) + __base_test_add_completions -W "$("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" return ;; esac @@ -226,7 +226,7 @@ _base_test_escaped_command() { # Offer positional completions case "${positional_number}" in 1) - COMPREPLY+=($(compgen -W "$("${COMP_WORDS[0]}" ---completion escaped-command -- two "${COMP_WORDS[@]}")" -- "${cur}")) + __base_test_add_completions -W "$("${COMP_WORDS[0]}" ---completion escaped-command -- two "${COMP_WORDS[@]}")" return ;; esac From 9d8bc409202d4d57e551c9169f54b1a4d048d046 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 26 Jan 2025 18:38:14 -0500 Subject: [PATCH 21/28] bash custom completion of empty word followed by other words. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 16 +++++++++++++++- .../testMathBashCompletionScript().bash | 14 ++++++++++++-- .../Snapshots/testBase_Bash().bash | 16 +++++++++++++--- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 57e1d78c8..adb7ff37c 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -143,6 +143,16 @@ extension [ParsableCommand.Type] { done < <(IFS=$'\\n' compgen "${@}" -- "${cur}") } + \(customCompleteFunctionName)() { + if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then + local -ar words=("${COMP_WORDS[@]}") + else + local -ar words=("${COMP_WORDS[@]::${COMP_CWORD}}" '' "${COMP_WORDS[@]:${COMP_CWORD}}") + fi + + "${COMP_WORDS[0]}" "${@}" "${words[@]}" + } + \(completionFunctions)\ complete -o filenames -F \(completionFunctionName().shellEscapeForVariableName()) \(commandName) """ @@ -366,7 +376,7 @@ extension [ParsableCommand.Type] { // Generate a call back into the command to retrieve a completions list return """ \(addCompletionsFunctionName) -W\ - "$("${COMP_WORDS[0]}" \(arg.customCompletionCall(self)) "${COMP_WORDS[@]}")" + "$(\(customCompleteFunctionName) \(arg.customCompletionCall(self)))" """ } @@ -379,6 +389,10 @@ extension [ParsableCommand.Type] { private var addCompletionsFunctionName: String { "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_add_completions" } + + private var customCompleteFunctionName: String { + "_\(prefix(1).completionFunctionName().shellEscapeForVariableName())_custom_complete" + } } extension ArgumentDefinition { diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index dd05504a9..844585c4c 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -122,6 +122,16 @@ __math_add_completions() { done < <(IFS=$'\n' compgen "${@}" -- "${cur}") } +__math_custom_complete() { + if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then + local -ar words=("${COMP_WORDS[@]}") + else + local -ar words=("${COMP_WORDS[@]::${COMP_CWORD}}" '' "${COMP_WORDS[@]:${COMP_CWORD}}") + fi + + "${COMP_WORDS[0]}" "${@}" "${words[@]}" +} + _math() { trap "$(shopt -p);$(shopt -po)" RETURN shopt -s extglob @@ -233,7 +243,7 @@ _math_stats_quantiles() { return ;; --custom) - __math_add_completions -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- --custom "${COMP_WORDS[@]}")" + __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- --custom)" return ;; esac @@ -245,7 +255,7 @@ _math_stats_quantiles() { return ;; 2) - __math_add_completions -W "$("${COMP_WORDS[0]}" ---completion stats quantiles -- customArg "${COMP_WORDS[@]}")" + __math_add_completions -W "$(__math_custom_complete ---completion stats quantiles -- customArg)" return ;; esac diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index eaabd4cc3..7195f07f7 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -122,6 +122,16 @@ __base_test_add_completions() { done < <(IFS=$'\n' compgen "${@}" -- "${cur}") } +__base_test_custom_complete() { + if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then + local -ar words=("${COMP_WORDS[@]}") + else + local -ar words=("${COMP_WORDS[@]::${COMP_CWORD}}" '' "${COMP_WORDS[@]:${COMP_CWORD}}") + fi + + "${COMP_WORDS[0]}" "${@}" "${words[@]}" +} + _base_test() { trap "$(shopt -p);$(shopt -po)" RETURN shopt -s extglob @@ -179,11 +189,11 @@ _base_test() { # Offer positional completions case "${positional_number}" in 1) - __base_test_add_completions -W "$("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" + __base_test_add_completions -W "$(__base_test_custom_complete ---completion -- argument)" return ;; 2) - __base_test_add_completions -W "$("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" + __base_test_add_completions -W "$(__base_test_custom_complete ---completion -- nested.nestedArgument)" return ;; esac @@ -226,7 +236,7 @@ _base_test_escaped_command() { # Offer positional completions case "${positional_number}" in 1) - __base_test_add_completions -W "$("${COMP_WORDS[0]}" ---completion escaped-command -- two "${COMP_WORDS[@]}")" + __base_test_add_completions -W "$(__base_test_custom_complete ---completion escaped-command -- two)" return ;; esac From 8935e009337054aeb2413cd3c388bfb81708a9e0 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:09:40 -0500 Subject: [PATCH 22/28] Prevent bash shellCommand completion scripting from breaking scripts. Eval the given command from a single-quoted string instead of running it directly in the shell. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/BashCompletionsGenerator.swift | 2 +- .../Snapshots/testMathBashCompletionScript().bash | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index adb7ff37c..a5ce08c40 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -368,7 +368,7 @@ extension [ParsableCommand.Type] { case .shellCommand(let command): return """ - COMPREPLY+=($(\(command))) + COMPREPLY+=($(eval '\(command.shellEscapeForSingleQuotedString())')) """ diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 844585c4c..424646f02 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -239,7 +239,7 @@ _math_stats_quantiles() { return ;; --shell) - COMPREPLY+=($(head -100 /usr/share/dict/words | tail -50)) + COMPREPLY+=($(eval 'head -100 /usr/share/dict/words | tail -50')) return ;; --custom) From 9331a5709ff08bcfb55837ab3facbde716971c6c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:41:55 -0500 Subject: [PATCH 23/28] Allow bash shellCommand completions to contain spaces. If existing shellCommand completions depend on spaces as completion delimiters, they will not work anymore. Newlines are now the only supported delimiters. Resolve #734 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/BashCompletionsGenerator.swift | 2 +- .../Snapshots/testMathBashCompletionScript().bash | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index a5ce08c40..2b8d5010a 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -368,7 +368,7 @@ extension [ParsableCommand.Type] { case .shellCommand(let command): return """ - COMPREPLY+=($(eval '\(command.shellEscapeForSingleQuotedString())')) + \(addCompletionsFunctionName) -W "$(eval '\(command.shellEscapeForSingleQuotedString())')" """ diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 424646f02..60e92bf83 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -239,7 +239,7 @@ _math_stats_quantiles() { return ;; --shell) - COMPREPLY+=($(eval 'head -100 /usr/share/dict/words | tail -50')) + __math_add_completions -W "$(eval 'head -100 /usr/share/dict/words | tail -50')" return ;; --custom) From 824d08c3975c65996bbd70952993f3a0884f7eb5 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 15 Feb 2025 20:10:35 -0500 Subject: [PATCH 24/28] =?UTF-8?q?Use=20zip(=E2=80=A6)=20to=20generate=20ba?= =?UTF-8?q?sh=20positional=20argument=20numbers.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 2b8d5010a..9ca89090f 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -246,14 +246,12 @@ extension [ParsableCommand.Type] { } if !positionalArguments.allSatisfy({ bashValueCompletion($0).isEmpty }) { - var position = 0 result += """ # Offer positional completions case "${positional_number}" in - \(positionalArguments - .compactMap { arg in - position += 1 + \(zip(1..., positionalArguments) + .compactMap { position, arg in let completion = bashValueCompletion(arg) return completion.isEmpty ? nil From e82cd673f4490e1251df00fe06b07a47b8520c04 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 15 Feb 2025 20:16:04 -0500 Subject: [PATCH 25/28] Rework bash positional case generation. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 9ca89090f..90d51d15b 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -245,26 +245,27 @@ extension [ParsableCommand.Type] { """ } - if !positionalArguments.allSatisfy({ bashValueCompletion($0).isEmpty }) { + let positionalCases = + zip(1..., positionalArguments) + .compactMap { position, arg in + let completion = bashValueCompletion(arg) + return completion.isEmpty + ? nil + : """ + \(position)) + \(completion.indentingEachLine(by: 8))\ + return + ;; + + """ + } + + if !positionalCases.isEmpty { result += """ # Offer positional completions case "${positional_number}" in - \(zip(1..., positionalArguments) - .compactMap { position, arg in - let completion = bashValueCompletion(arg) - return completion.isEmpty - ? nil - : """ - \(position)) - \(completion.indentingEachLine(by: 8))\ - return - ;; - - """ - } - .joined() - )\ + \(positionalCases.joined())\ esac """ From db50e6ea809e45e74361a944698fd389ad49ac3b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:08:59 -0500 Subject: [PATCH 26/28] Reverse the polarity of the neutron flow of the completions help subcommand detector. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/ArgumentParser/Completions/CompletionsGenerator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index ac0edc254..010a2235d 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -187,7 +187,7 @@ extension [ParsableCommand.Type] { /// Include default 'help' subcommand in nonempty subcommand list if & only if /// no help subcommand already exists. mutating func addHelpSubcommandIfMissing() { - if !isEmpty && allSatisfy({ $0._commandName != "help" }) { + if !isEmpty && !contains(where: { $0._commandName == "help" }) { append(HelpCommand.self) } } From 88dcf7e005d1cf81d6bd37c13b55173b521fc0ac Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:21:58 -0500 Subject: [PATCH 27/28] Rename flags & options vars to flagCompletions & optionCompletions, respectively, in BashCompletionsGenerator.swift. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 90d51d15b..1143dae8e 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -203,12 +203,12 @@ extension [ParsableCommand.Type] { let positionalArguments = positionalArguments - let flags = flags - let options = options - if !flags.isEmpty || !options.isEmpty { + let flagCompletions = flagCompletions + let optionCompletions = optionCompletions + if !flagCompletions.isEmpty || !optionCompletions.isEmpty { result += """ - \(declareTopLevelArray)flags=(\(flags.joined(separator: " "))) - \(declareTopLevelArray)options=(\(options.joined(separator: " "))) + \(declareTopLevelArray)flags=(\(flagCompletions.joined(separator: " "))) + \(declareTopLevelArray)options=(\(optionCompletions.joined(separator: " "))) \(offerFlagsOptionsFunctionName) \(positionalArguments.count) """ @@ -307,8 +307,8 @@ extension [ParsableCommand.Type] { """ } - /// Returns flags for the last command of the given array. - private var flags: [String] { + /// Returns flag completions for the last command of the given array. + private var flagCompletions: [String] { argumentsForHelp(visibility: .default).flatMap { switch ($0.kind, $0.update) { case (.named, .nullary): @@ -319,8 +319,8 @@ extension [ParsableCommand.Type] { } } - /// Returns options for the last command of the given array. - private var options: [String] { + /// Returns option completions for the last command of the given array. + private var optionCompletions: [String] { argumentsForHelp(visibility: .default).flatMap { switch ($0.kind, $0.update) { case (.named, .unary): From 0ad79110d1c9b0506c9d608a17d122384a703d4b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 15 Feb 2025 21:24:09 -0500 Subject: [PATCH 28/28] Remove TODO comments from generated bash completion scripts. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/BashCompletionsGenerator.swift | 7 ------- .../Snapshots/testMathBashCompletionScript().bash | 8 -------- .../Snapshots/testBase_Bash().bash | 8 -------- 3 files changed, 23 deletions(-) diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index 1143dae8e..f89c2ec1d 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -71,16 +71,12 @@ extension [ParsableCommand.Type] { -*) # ${word} is a flag or an option # If ${word} is an option, mark that the next word to be parsed is an option value - # TODO: handle joined-value options (-o=file.ext), stacked flags (-aBc), legacy long (-long), combos - # TODO: if multi-valued options can exist, support them local option for option in "${options[@]}"; do [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break done # Remove ${word} from ${flags} or ${options} so it isn't offered again - # TODO: handle repeatable flags & options - # TODO: remove equivalent options (-h/--help) & exclusive options (--yes/--no) local not_found=true local -i index for index in "${!flags[@]}"; do @@ -107,7 +103,6 @@ extension [ParsableCommand.Type] { fi # ${word} is neither a flag, nor an option, nor an option value - # TODO: can SAP be configured to require options before positionals? if [[ "${positional_number}" -lt "${positional_count}" ]]; then # ${word} is a positional ((positional_number++)) @@ -126,7 +121,6 @@ extension [ParsableCommand.Type] { unparsed_words=("${unparsed_words[@]}") - # TODO: offer flags & options after all positionals iff they're allowed after positionals if\\ ! "${was_flag_option_terminator_seen}"\\ && ! "${is_parsing_option_value}"\\ @@ -237,7 +231,6 @@ extension [ParsableCommand.Type] { result += """ # Offer option value completions - # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in \(optionHandlers) esac diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash index 60e92bf83..b88219d2e 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathBashCompletionScript().bash @@ -50,16 +50,12 @@ __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 - # TODO: handle joined-value options (-o=file.ext), stacked flags (-aBc), legacy long (-long), combos - # TODO: if multi-valued options can exist, support them local option for option in "${options[@]}"; do [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break done # Remove ${word} from ${flags} or ${options} so it isn't offered again - # TODO: handle repeatable flags & options - # TODO: remove equivalent options (-h/--help) & exclusive options (--yes/--no) local not_found=true local -i index for index in "${!flags[@]}"; do @@ -86,7 +82,6 @@ __math_offer_flags_options() { fi # ${word} is neither a flag, nor an option, nor an option value - # TODO: can SAP be configured to require options before positionals? if [[ "${positional_number}" -lt "${positional_count}" ]]; then # ${word} is a positional ((positional_number++)) @@ -105,7 +100,6 @@ __math_offer_flags_options() { unparsed_words=("${unparsed_words[@]}") - # TODO: offer flags & options after all positionals iff they're allowed after positionals if\ ! "${was_flag_option_terminator_seen}"\ && ! "${is_parsing_option_value}"\ @@ -207,7 +201,6 @@ _math_stats_average() { __math_offer_flags_options 1 # Offer option value completions - # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --kind) __math_add_completions -W 'mean'$'\n''median'$'\n''mode' @@ -228,7 +221,6 @@ _math_stats_quantiles() { __math_offer_flags_options 3 # Offer option value completions - # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --file) __math_add_completions -o plusdirs -fX '!*.@(txt|md)' diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 7195f07f7..8da5e1893 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -50,16 +50,12 @@ __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 - # TODO: handle joined-value options (-o=file.ext), stacked flags (-aBc), legacy long (-long), combos - # TODO: if multi-valued options can exist, support them local option for option in "${options[@]}"; do [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break done # Remove ${word} from ${flags} or ${options} so it isn't offered again - # TODO: handle repeatable flags & options - # TODO: remove equivalent options (-h/--help) & exclusive options (--yes/--no) local not_found=true local -i index for index in "${!flags[@]}"; do @@ -86,7 +82,6 @@ __base_test_offer_flags_options() { fi # ${word} is neither a flag, nor an option, nor an option value - # TODO: can SAP be configured to require options before positionals? if [[ "${positional_number}" -lt "${positional_count}" ]]; then # ${word} is a positional ((positional_number++)) @@ -105,7 +100,6 @@ __base_test_offer_flags_options() { unparsed_words=("${unparsed_words[@]}") - # TODO: offer flags & options after all positionals iff they're allowed after positionals if\ ! "${was_flag_option_terminator_seen}"\ && ! "${is_parsing_option_value}"\ @@ -153,7 +147,6 @@ _base_test() { __base_test_offer_flags_options 2 # Offer option value completions - # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --name) return @@ -226,7 +219,6 @@ _base_test_escaped_command() { __base_test_offer_flags_options 1 # Offer option value completions - # TODO: only if ${prev} matches -* & is not an option value case "${prev}" in --one) return