From 5f5446b5f899f0db22e3f733a94b3187265211e2 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:50:00 -0500 Subject: [PATCH 01/33] Do not indent zsh cases. Simplify zsh indent generation. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 15 ++-- .../testMathZshCompletionScript().zsh | 88 +++++++++---------- .../Snapshots/testBase_Zsh().zsh | 40 ++++----- 3 files changed, 70 insertions(+), 73 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 9c5469091..c4425e5e2 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -51,21 +51,19 @@ struct ZshCompletionsGenerator { let subcommandModes = subcommands.map { """ - '\($0._commandName):\($0.configuration.abstract.zshEscaped())' + '\($0._commandName):\($0.configuration.abstract.zshEscaped())' """ - .indentingEachLine(by: 12) } let subcommandArgs = subcommands.map { """ - (\($0._commandName)) - \(functionName)_\($0._commandName) - ;; + (\($0._commandName)) + \(functionName)_\($0._commandName) + ;; """ - .indentingEachLine(by: 12) } subcommandHandler = """ - case $state in + case $state in (command) local subcommands subcommands=( @@ -78,10 +76,9 @@ struct ZshCompletionsGenerator { \(subcommandArgs.joined(separator: "\n")) esac ;; - esac + esac """ - .indentingEachLine(by: 4) } let functionText = """ diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index ba5b1db74..cb39616de 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -17,32 +17,32 @@ _math() { ) _arguments -w -s -S $args[@] && ret=0 case $state in - (command) - local subcommands - subcommands=( - 'add:Print the sum of the values.' - 'multiply:Print the product of the values.' - 'stats:Calculate descriptive statistics.' - 'help:Show subcommand help information.' - ) - _describe "subcommand" subcommands + (command) + local subcommands + subcommands=( + 'add:Print the sum of the values.' + 'multiply:Print the product of the values.' + 'stats:Calculate descriptive statistics.' + 'help:Show subcommand help information.' + ) + _describe "subcommand" subcommands + ;; + (arg) + case ${words[1]} in + (add) + _math_add ;; - (arg) - case ${words[1]} in - (add) - _math_add - ;; - (multiply) - _math_multiply - ;; - (stats) - _math_stats - ;; - (help) - _math_help - ;; - esac + (multiply) + _math_multiply ;; + (stats) + _math_stats + ;; + (help) + _math_help + ;; + esac + ;; esac return ret @@ -87,28 +87,28 @@ _math_stats() { ) _arguments -w -s -S $args[@] && ret=0 case $state in - (command) - local subcommands - subcommands=( - 'average:Print the average of the values.' - 'stdev:Print the standard deviation of the values.' - 'quantiles:Print the quantiles of the values (TBD).' - ) - _describe "subcommand" subcommands + (command) + local subcommands + subcommands=( + 'average:Print the average of the values.' + 'stdev:Print the standard deviation of the values.' + 'quantiles:Print the quantiles of the values (TBD).' + ) + _describe "subcommand" subcommands + ;; + (arg) + case ${words[1]} in + (average) + _math_stats_average + ;; + (stdev) + _math_stats_stdev ;; - (arg) - case ${words[1]} in - (average) - _math_stats_average - ;; - (stdev) - _math_stats_stdev - ;; - (quantiles) - _math_stats_quantiles - ;; - esac + (quantiles) + _math_stats_quantiles ;; + esac + ;; esac return ret diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 36c522302..b4dbb2c73 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -30,28 +30,28 @@ _base-test() { ) _arguments -w -s -S $args[@] && ret=0 case $state in - (command) - local subcommands - subcommands=( - 'sub-command:' - 'escaped-command:' - 'help:Show subcommand help information.' - ) - _describe "subcommand" subcommands + (command) + local subcommands + subcommands=( + 'sub-command:' + 'escaped-command:' + 'help:Show subcommand help information.' + ) + _describe "subcommand" subcommands + ;; + (arg) + case ${words[1]} in + (sub-command) + _base-test_sub-command ;; - (arg) - case ${words[1]} in - (sub-command) - _base-test_sub-command - ;; - (escaped-command) - _base-test_escaped-command - ;; - (help) - _base-test_help - ;; - esac + (escaped-command) + _base-test_escaped-command ;; + (help) + _base-test_help + ;; + esac + ;; esac return ret From e22f251bd7512af2a06d5d5126c96b2feab9c670 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:55:15 -0500 Subject: [PATCH 02/33] Do not prefix zsh cases with an open parenthesis. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 6 ++--- .../testMathZshCompletionScript().zsh | 22 +++++++++---------- .../Snapshots/testBase_Zsh().zsh | 10 ++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index c4425e5e2..e407b2736 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -56,7 +56,7 @@ struct ZshCompletionsGenerator { } let subcommandArgs = subcommands.map { """ - (\($0._commandName)) + \($0._commandName)) \(functionName)_\($0._commandName) ;; """ @@ -64,14 +64,14 @@ struct ZshCompletionsGenerator { subcommandHandler = """ case $state in - (command) + command) local subcommands subcommands=( \(subcommandModes.joined(separator: "\n")) ) _describe "subcommand" subcommands ;; - (arg) + arg) case ${words[1]} in \(subcommandArgs.joined(separator: "\n")) esac diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index cb39616de..076120915 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -17,7 +17,7 @@ _math() { ) _arguments -w -s -S $args[@] && ret=0 case $state in - (command) + command) local subcommands subcommands=( 'add:Print the sum of the values.' @@ -27,18 +27,18 @@ _math() { ) _describe "subcommand" subcommands ;; - (arg) + arg) case ${words[1]} in - (add) + add) _math_add ;; - (multiply) + multiply) _math_multiply ;; - (stats) + stats) _math_stats ;; - (help) + help) _math_help ;; esac @@ -87,7 +87,7 @@ _math_stats() { ) _arguments -w -s -S $args[@] && ret=0 case $state in - (command) + command) local subcommands subcommands=( 'average:Print the average of the values.' @@ -96,15 +96,15 @@ _math_stats() { ) _describe "subcommand" subcommands ;; - (arg) + arg) case ${words[1]} in - (average) + average) _math_stats_average ;; - (stdev) + stdev) _math_stats_stdev ;; - (quantiles) + quantiles) _math_stats_quantiles ;; esac diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index b4dbb2c73..705688ea9 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -30,7 +30,7 @@ _base-test() { ) _arguments -w -s -S $args[@] && ret=0 case $state in - (command) + command) local subcommands subcommands=( 'sub-command:' @@ -39,15 +39,15 @@ _base-test() { ) _describe "subcommand" subcommands ;; - (arg) + arg) case ${words[1]} in - (sub-command) + sub-command) _base-test_sub-command ;; - (escaped-command) + escaped-command) _base-test_escaped-command ;; - (help) + help) _base-test_help ;; esac From beaa419b6f74e2d7464d6d57336ad3986a989c24 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:13:15 -0500 Subject: [PATCH 03/33] Prevent zsh parameter word splitting. Brace & quote parameter uses. Use [@] for quoted array output. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 12 ++--- .../testMathZshCompletionScript().zsh | 46 +++++++++---------- .../Snapshots/testBase_Zsh().zsh | 28 +++++------ 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index e407b2736..2971977b2 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -17,7 +17,7 @@ struct ZshCompletionsGenerator { return """ #compdef \(type._commandName) local context state state_descr line - _\(type._commandName.zshEscapingCommandName())_commandname=$words[1] + _\(type._commandName.zshEscapingCommandName())_commandname="${words[1]}" typeset -A opt_args \(generateCompletionFunction([type])) @@ -63,7 +63,7 @@ struct ZshCompletionsGenerator { } subcommandHandler = """ - case $state in + case "${state}" in command) local subcommands subcommands=( @@ -72,7 +72,7 @@ struct ZshCompletionsGenerator { _describe "subcommand" subcommands ;; arg) - case ${words[1]} in + case "${words[1]}" in \(subcommandArgs.joined(separator: "\n")) esac ;; @@ -93,9 +93,9 @@ struct ZshCompletionsGenerator { args+=( \(args.joined(separator: "\n").indentingEachLine(by: 8)) ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 \(subcommandHandler) - return ret + return "${ret}" } @@ -216,7 +216,7 @@ extension ArgumentDefinition { // Generate a call back into the command to retrieve a completions list let commandName = type._commandName.zshEscapingCommandName() return - "{_custom_completion $_\(commandName)_commandname \(customCompletionCall(commands)) $words}" + "{_custom_completion \"${_\(commandName)_commandname}\" \(customCompletionCall(commands)) \"${words[@]}\"}" } } } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 076120915..020b2e7d2 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -1,6 +1,6 @@ #compdef math local context state state_descr line -_math_commandname=$words[1] +_math_commandname="${words[1]}" typeset -A opt_args _math() { @@ -15,8 +15,8 @@ _math() { '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S $args[@] && ret=0 - case $state in + _arguments -w -s -S "${args[@]}" && ret=0 + case "${state}" in command) local subcommands subcommands=( @@ -28,7 +28,7 @@ _math() { _describe "subcommand" subcommands ;; arg) - case ${words[1]} in + case "${words[1]}" in add) _math_add ;; @@ -45,7 +45,7 @@ _math() { ;; esac - return ret + return "${ret}" } _math_add() { @@ -57,9 +57,9 @@ _math_add() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } _math_multiply() { @@ -71,9 +71,9 @@ _math_multiply() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } _math_stats() { @@ -85,8 +85,8 @@ _math_stats() { '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S $args[@] && ret=0 - case $state in + _arguments -w -s -S "${args[@]}" && ret=0 + case "${state}" in command) local subcommands subcommands=( @@ -97,7 +97,7 @@ _math_stats() { _describe "subcommand" subcommands ;; arg) - case ${words[1]} in + case "${words[1]}" in average) _math_stats_average ;; @@ -111,7 +111,7 @@ _math_stats() { ;; esac - return ret + return "${ret}" } _math_stats_average() { @@ -123,9 +123,9 @@ _math_stats_average() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } _math_stats_stdev() { @@ -136,9 +136,9 @@ _math_stats_stdev() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } _math_stats_quantiles() { @@ -146,18 +146,18 @@ _math_stats_quantiles() { local -a args args+=( ':one-of-four:(alphabet alligator branch braggart)' - ':custom-arg:{_custom_completion $_math_commandname ---completion stats quantiles -- customArg $words}' + ':custom-arg:{_custom_completion "${_math_commandname}" ---completion stats quantiles -- customArg "${words[@]}"}' ':values:' '--file:file:_files -g '"'"'*.txt *.md'"'"'' '--directory:directory:_files -/' '--shell:shell:{local -a list; list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"}); _describe '''' list}' - '--custom:custom:{_custom_completion $_math_commandname ---completion stats quantiles -- --custom $words}' + '--custom:custom:{_custom_completion "${_math_commandname}" ---completion stats quantiles -- --custom "${words[@]}"}' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } _math_help() { @@ -167,9 +167,9 @@ _math_help() { ':subcommands:' '--version[Show the version.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 705688ea9..7f93eb7bc 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -1,6 +1,6 @@ #compdef base-test local context state state_descr line -_base_test_commandname=$words[1] +_base_test_commandname="${words[1]}" typeset -A opt_args _base-test() { @@ -22,14 +22,14 @@ _base-test() { '*--kind-counter' '*--rep1:rep1:' '*'{-r,--rep2}':rep2:' - ':argument:{_custom_completion $_base_test_commandname ---completion -- argument $words}' - ':nested-argument:{_custom_completion $_base_test_commandname ---completion -- nested.nestedArgument $words}' + ':argument:{_custom_completion "${_base_test_commandname}" ---completion -- argument "${words[@]}"}' + ':nested-argument:{_custom_completion "${_base_test_commandname}" ---completion -- nested.nestedArgument "${words[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S $args[@] && ret=0 - case $state in + _arguments -w -s -S "${args[@]}" && ret=0 + case "${state}" in command) local subcommands subcommands=( @@ -40,7 +40,7 @@ _base-test() { _describe "subcommand" subcommands ;; arg) - case ${words[1]} in + case "${words[1]}" in sub-command) _base-test_sub-command ;; @@ -54,7 +54,7 @@ _base-test() { ;; esac - return ret + return "${ret}" } _base-test_sub-command() { @@ -63,9 +63,9 @@ _base-test_sub-command() { args+=( '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } _base-test_escaped-command() { @@ -73,12 +73,12 @@ _base-test_escaped-command() { local -a args args+=( '--one[Escaped chars: '"'"'\[\]\\.]:one:' - ':two:{_custom_completion $_base_test_commandname ---completion escaped-command -- two $words}' + ':two:{_custom_completion "${_base_test_commandname}" ---completion escaped-command -- two "${words[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } _base-test_help() { @@ -87,9 +87,9 @@ _base-test_help() { args+=( ':subcommands:' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S "${args[@]}" && ret=0 - return ret + return "${ret}" } From 480f77298f2ed6fe721aa78286d57cc8be30c3a2 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:14:37 -0500 Subject: [PATCH 04/33] Improve comment in ZshCompletionsGenerator.swift. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/ZshCompletionsGenerator.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 2971977b2..94863e802 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -175,7 +175,8 @@ extension ArgumentDefinition { return "'\(line)\(inputs)'" } - /// - returns: `true` if I'm an option and can be tab-completed multiple times in one command line. For example, `ssh` allows the `-L` option to be given multiple times, to establish multiple port forwardings. + /// - returns: `true` if `self` is an option and can be tab-completed multiple times in one command line. + /// For example, `ssh` allows the `-L` option to be given multiple times, to establish multiple port forwardings. private var isRepeatableOption: Bool { guard case .named(_) = kind, From 0eae18f0fe2bedfb74187f9e9ee539e11de3e736 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:22:25 -0500 Subject: [PATCH 05/33] Fix incorrect zsh shellCommand single quotes: 4 consecutive single quotes were obviously intended to be 2 escaped single quotes, but that isn't zsh syntax. Use 2 double quotes instead. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/ZshCompletionsGenerator.swift | 2 +- .../Snapshots/testMathZshCompletionScript().zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 94863e802..77593f9d2 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -210,7 +210,7 @@ extension ArgumentDefinition { case .shellCommand(let command): return - "{local -a list; list=(${(f)\"$(\(command))\"}); _describe '''' list}" + "{local -a list;list=(${(f)\"$(\(command))\"});_describe \"\" list}" case .custom: guard let type = commands.first else { return "" } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 020b2e7d2..cd40b70fb 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -150,7 +150,7 @@ _math_stats_quantiles() { ':values:' '--file:file:_files -g '"'"'*.txt *.md'"'"'' '--directory:directory:_files -/' - '--shell:shell:{local -a list; list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"}); _describe '''' list}' + '--shell:shell:{local -a list;list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"});_describe "" list}' '--custom:custom:{_custom_completion "${_math_commandname}" ---completion stats quantiles -- --custom "${words[@]}"}' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' From b4b492a1a5a403a6b06a735db2543256f6716669 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:09:15 -0500 Subject: [PATCH 06/33] Remove extraneous zsh newline. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/ZshCompletionsGenerator.swift | 2 +- .../Snapshots/testMathZshCompletionScript().zsh | 1 - Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 77593f9d2..39f48abb2 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -20,7 +20,7 @@ struct ZshCompletionsGenerator { _\(type._commandName.zshEscapingCommandName())_commandname="${words[1]}" typeset -A opt_args - \(generateCompletionFunction([type])) + \(generateCompletionFunction([type]))\ _custom_completion() { local completions=("${(@f)$($*)}") _describe '' completions diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index cd40b70fb..f5ec72024 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -172,7 +172,6 @@ _math_help() { return "${ret}" } - _custom_completion() { local completions=("${(@f)$($*)}") _describe '' completions diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 7f93eb7bc..8b8df8633 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -92,7 +92,6 @@ _base-test_help() { return "${ret}" } - _custom_completion() { local completions=("${(@f)$($*)}") _describe '' completions From dbbb99eeab2cf3a5fe9e71a0435d20616ab75e20 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:26:43 -0500 Subject: [PATCH 07/33] Improve zsh variable declarations: scoping, typing & readonly. Remove trailing spaces from InstallingCompletionScripts.md. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 34 ++++++----- .../Articles/InstallingCompletionScripts.md | 7 +-- .../testMathZshCompletionScript().zsh | 60 +++++++++---------- .../Snapshots/testBase_Zsh().zsh | 37 ++++++------ 4 files changed, 66 insertions(+), 72 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 39f48abb2..aa36cffd3 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -16,9 +16,6 @@ struct ZshCompletionsGenerator { return """ #compdef \(type._commandName) - local context state state_descr line - _\(type._commandName.zshEscapingCommandName())_commandname="${words[1]}" - typeset -A opt_args \(generateCompletionFunction([type]))\ _custom_completion() { @@ -65,8 +62,7 @@ struct ZshCompletionsGenerator { subcommandHandler = """ case "${state}" in command) - local subcommands - subcommands=( + local -ar subcommands=( \(subcommandModes.joined(separator: "\n")) ) _describe "subcommand" subcommands @@ -82,15 +78,25 @@ struct ZshCompletionsGenerator { } let functionText = """ - \(functionName)() {\(isRootCommand ? """ - - export \(CompletionShell.shellEnvironmentVariableName)=zsh - \(CompletionShell.shellVersionEnvironmentVariableName)="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" - export \(CompletionShell.shellVersionEnvironmentVariableName) - """ : "") - integer ret=1 - local -a args - args+=( + \(functionName)() { + \(isRootCommand + ? """ + local -xr \(CompletionShell.shellEnvironmentVariableName)=zsh + local -x \(CompletionShell.shellVersionEnvironmentVariableName) + \(CompletionShell.shellVersionEnvironmentVariableName)="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" + local -r \(CompletionShell.shellVersionEnvironmentVariableName) + + local context state state_descr line + local -A opt_args + + local -r _\(type._commandName.zshEscapingCommandName())_commandname="${words[1]}" + + + """ + : "" + )\ + local -i ret=1 + local -ar args=( \(args.joined(separator: "\n").indentingEachLine(by: 8)) ) _arguments -w -s -S "${args[@]}" && ret=0 diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md b/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md index 24265bcaf..e460ed623 100644 --- a/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md @@ -1,6 +1,6 @@ # Generating and Installing Completion Scripts -Install shell completion scripts generated by your command-line tool. +Install shell completion scripts generated by your command-line tool. ## Overview @@ -9,13 +9,8 @@ Command-line tools that you build with `ArgumentParser` include a built-in optio ``` $ example --generate-completion-script bash #compdef example -local context state state_descr line -_example_commandname="example" -typeset -A opt_args _example() { - integer ret=1 - local -a args ... } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index f5ec72024..95467da92 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -1,15 +1,18 @@ #compdef math -local context state state_descr line -_math_commandname="${words[1]}" -typeset -A opt_args _math() { - export SAP_SHELL=zsh + local -xr SAP_SHELL=zsh + local -x SAP_SHELL_VERSION SAP_SHELL_VERSION="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" - export SAP_SHELL_VERSION - integer ret=1 - local -a args - args+=( + local -r SAP_SHELL_VERSION + + local context state state_descr line + local -A opt_args + + local -r _math_commandname="${words[1]}" + + local -i ret=1 + local -ar args=( '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' @@ -18,8 +21,7 @@ _math() { _arguments -w -s -S "${args[@]}" && ret=0 case "${state}" in command) - local subcommands - subcommands=( + local -ar subcommands=( 'add:Print the sum of the values.' 'multiply:Print the product of the values.' 'stats:Calculate descriptive statistics.' @@ -49,9 +51,8 @@ _math() { } _math_add() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' ':values:' '--version[Show the version.]' @@ -63,9 +64,8 @@ _math_add() { } _math_multiply() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' ':values:' '--version[Show the version.]' @@ -77,9 +77,8 @@ _math_multiply() { } _math_stats() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' @@ -88,8 +87,7 @@ _math_stats() { _arguments -w -s -S "${args[@]}" && ret=0 case "${state}" in command) - local subcommands - subcommands=( + local -ar subcommands=( 'average:Print the average of the values.' 'stdev:Print the standard deviation of the values.' 'quantiles:Print the quantiles of the values (TBD).' @@ -115,9 +113,8 @@ _math_stats() { } _math_stats_average() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( '--kind[The kind of average to provide.]:kind:(mean median mode)' ':values:' '--version[Show the version.]' @@ -129,9 +126,8 @@ _math_stats_average() { } _math_stats_stdev() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' @@ -142,9 +138,8 @@ _math_stats_stdev() { } _math_stats_quantiles() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( ':one-of-four:(alphabet alligator branch braggart)' ':custom-arg:{_custom_completion "${_math_commandname}" ---completion stats quantiles -- customArg "${words[@]}"}' ':values:' @@ -161,9 +156,8 @@ _math_stats_quantiles() { } _math_help() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( ':subcommands:' '--version[Show the version.]' ) diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 8b8df8633..aa2b84dfd 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -1,15 +1,18 @@ #compdef base-test -local context state state_descr line -_base_test_commandname="${words[1]}" -typeset -A opt_args _base-test() { - export SAP_SHELL=zsh + local -xr SAP_SHELL=zsh + local -x SAP_SHELL_VERSION SAP_SHELL_VERSION="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" - export SAP_SHELL_VERSION - integer ret=1 - local -a args - args+=( + local -r SAP_SHELL_VERSION + + local context state state_descr line + local -A opt_args + + local -r _base_test_commandname="${words[1]}" + + local -i ret=1 + local -ar args=( '--name[The user'"'"'s name.]:name:' '--kind:kind:(one two custom-three)' '--other-kind:other-kind:(b1_zsh b2_zsh b3_zsh)' @@ -31,8 +34,7 @@ _base-test() { _arguments -w -s -S "${args[@]}" && ret=0 case "${state}" in command) - local subcommands - subcommands=( + local -ar subcommands=( 'sub-command:' 'escaped-command:' 'help:Show subcommand help information.' @@ -58,9 +60,8 @@ _base-test() { } _base-test_sub-command() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S "${args[@]}" && ret=0 @@ -69,9 +70,8 @@ _base-test_sub-command() { } _base-test_escaped-command() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( '--one[Escaped chars: '"'"'\[\]\\.]:one:' ':two:{_custom_completion "${_base_test_commandname}" ---completion escaped-command -- two "${words[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' @@ -82,9 +82,8 @@ _base-test_escaped-command() { } _base-test_help() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar args=( ':subcommands:' ) _arguments -w -s -S "${args[@]}" && ret=0 From 69f2d731a3cb4f7a5f083f20401e725f82553ba2 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 26 Jan 2025 20:22:08 -0500 Subject: [PATCH 08/33] Include zsh words before current subcommand in custom completion arg. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 11 +++-------- .../Snapshots/testMathZshCompletionScript().zsh | 7 ++++--- .../Snapshots/testBase_Zsh().zsh | 9 +++++---- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index aa36cffd3..7f1a29066 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -89,7 +89,8 @@ struct ZshCompletionsGenerator { local context state state_descr line local -A opt_args - local -r _\(type._commandName.zshEscapingCommandName())_commandname="${words[1]}" + local -r command_name="${words[1]}" + local -ar command_line=("${words[@]}") """ @@ -135,10 +136,6 @@ extension String { fileprivate func zshEscaped() -> String { self.zshEscapingSingleQuotes().zshEscapingMetacharacters() } - - fileprivate func zshEscapingCommandName() -> String { - self.replacingOccurrences(of: "-", with: "_") - } } extension ArgumentDefinition { @@ -219,11 +216,9 @@ extension ArgumentDefinition { "{local -a list;list=(${(f)\"$(\(command))\"});_describe \"\" list}" case .custom: - guard let type = commands.first else { return "" } // Generate a call back into the command to retrieve a completions list - let commandName = type._commandName.zshEscapingCommandName() return - "{_custom_completion \"${_\(commandName)_commandname}\" \(customCompletionCall(commands)) \"${words[@]}\"}" + "{_custom_completion \"${command_name}\" \(customCompletionCall(commands)) \"${command_line[@]}\"}" } } } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 95467da92..c7e024f92 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -9,7 +9,8 @@ _math() { local context state state_descr line local -A opt_args - local -r _math_commandname="${words[1]}" + local -r command_name="${words[1]}" + local -ar command_line=("${words[@]}") local -i ret=1 local -ar args=( @@ -141,12 +142,12 @@ _math_stats_quantiles() { local -i ret=1 local -ar args=( ':one-of-four:(alphabet alligator branch braggart)' - ':custom-arg:{_custom_completion "${_math_commandname}" ---completion stats quantiles -- customArg "${words[@]}"}' + ':custom-arg:{_custom_completion "${command_name}" ---completion stats quantiles -- customArg "${command_line[@]}"}' ':values:' '--file:file:_files -g '"'"'*.txt *.md'"'"'' '--directory:directory:_files -/' '--shell:shell:{local -a list;list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"});_describe "" list}' - '--custom:custom:{_custom_completion "${_math_commandname}" ---completion stats quantiles -- --custom "${words[@]}"}' + '--custom:custom:{_custom_completion "${command_name}" ---completion stats quantiles -- --custom "${command_line[@]}"}' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index aa2b84dfd..6aea5547d 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -9,7 +9,8 @@ _base-test() { local context state state_descr line local -A opt_args - local -r _base_test_commandname="${words[1]}" + local -r command_name="${words[1]}" + local -ar command_line=("${words[@]}") local -i ret=1 local -ar args=( @@ -25,8 +26,8 @@ _base-test() { '*--kind-counter' '*--rep1:rep1:' '*'{-r,--rep2}':rep2:' - ':argument:{_custom_completion "${_base_test_commandname}" ---completion -- argument "${words[@]}"}' - ':nested-argument:{_custom_completion "${_base_test_commandname}" ---completion -- nested.nestedArgument "${words[@]}"}' + ':argument:{_custom_completion "${command_name}" ---completion -- argument "${command_line[@]}"}' + ':nested-argument:{_custom_completion "${command_name}" ---completion -- nested.nestedArgument "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' @@ -73,7 +74,7 @@ _base-test_escaped-command() { local -i ret=1 local -ar args=( '--one[Escaped chars: '"'"'\[\]\\.]:one:' - ':two:{_custom_completion "${_base_test_commandname}" ---completion escaped-command -- two "${words[@]}"}' + ':two:{_custom_completion "${command_name}" ---completion escaped-command -- two "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S "${args[@]}" && ret=0 From 17012b56f2b3b48ed3a923ab5680764a6b5a5061 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:28:13 -0500 Subject: [PATCH 09/33] Make subcommandHandler in ZshCompletionsGenerator.swift immutable. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 7f1a29066..59e5bd8e5 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -37,8 +37,11 @@ struct ZshCompletionsGenerator { var args = generateCompletionArguments(commands) var subcommands = type.configuration.subcommands .filter { $0.configuration.shouldDisplay } - var subcommandHandler = "" - if !subcommands.isEmpty { + + let subcommandHandler: String + if subcommands.isEmpty { + subcommandHandler = "" + } else { args.append("'(-): :->command'") args.append("'(-)*:: :->arg'") From a6fd712ca18ee5a2c6ba19d14ea2ae5910f735fc Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 16:51:04 -0500 Subject: [PATCH 10/33] Escape zsh single quotes via '\'' instead of via '"'"'. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 9 +++++---- .../Snapshots/testMathZshCompletionScript().zsh | 2 +- .../ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 59e5bd8e5..cc7bd563a 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -128,16 +128,17 @@ struct ZshCompletionsGenerator { extension String { fileprivate func zshEscapingSingleQuotes() -> String { - self.replacingOccurrences(of: "'", with: #"'"'"'"#) + replacingOccurrences(of: "'", with: "'\\''") } fileprivate func zshEscapingMetacharacters() -> String { - self.replacingOccurrences( - of: #"[\\\[\]]"#, with: #"\\$0"#, options: .regularExpression) + replacingOccurrences( + of: #"[\\\[\]]"#, with: #"\\$0"#, options: .regularExpression + ) } fileprivate func zshEscaped() -> String { - self.zshEscapingSingleQuotes().zshEscapingMetacharacters() + zshEscapingMetacharacters().zshEscapingSingleQuotes() } } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index c7e024f92..25ed62d8c 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -144,7 +144,7 @@ _math_stats_quantiles() { ':one-of-four:(alphabet alligator branch braggart)' ':custom-arg:{_custom_completion "${command_name}" ---completion stats quantiles -- customArg "${command_line[@]}"}' ':values:' - '--file:file:_files -g '"'"'*.txt *.md'"'"'' + '--file:file:_files -g '\''*.txt *.md'\''' '--directory:directory:_files -/' '--shell:shell:{local -a list;list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"});_describe "" list}' '--custom:custom:{_custom_completion "${command_name}" ---completion stats quantiles -- --custom "${command_line[@]}"}' diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 6aea5547d..b78acd869 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -14,7 +14,7 @@ _base-test() { local -i ret=1 local -ar args=( - '--name[The user'"'"'s name.]:name:' + '--name[The user'\''s name.]:name:' '--kind:kind:(one two custom-three)' '--other-kind:other-kind:(b1_zsh b2_zsh b3_zsh)' '--path1:path1:_files' @@ -73,7 +73,7 @@ _base-test_sub-command() { _base-test_escaped-command() { local -i ret=1 local -ar args=( - '--one[Escaped chars: '"'"'\[\]\\.]:one:' + '--one[Escaped chars: '\''\[\]\\.]:one:' ':two:{_custom_completion "${command_name}" ---completion escaped-command -- two "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' ) From 7df50bee4e574bc398f536fc2f53b79ccababada Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 16:41:41 -0500 Subject: [PATCH 11/33] Escape single quotes in zsh shellCommand String. If someone already escapes single quotes from the String, this will cause an issue, but no one should be required to go to the trouble to manually escape single quotes in their script, especially since the requirement isn't documented or normal. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/ZshCompletionsGenerator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index cc7bd563a..7201e8019 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -217,7 +217,7 @@ extension ArgumentDefinition { case .shellCommand(let command): return - "{local -a list;list=(${(f)\"$(\(command))\"});_describe \"\" list}" + "{local -a list;list=(${(f)\"$(\(command.zshEscapingSingleQuotes()))\"});_describe \"\" list}" case .custom: // Generate a call back into the command to retrieve a completions list From ea1047f3fe16b90b82f64be60b2eb6b6526052c3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 17 Jan 2025 05:32:16 -0500 Subject: [PATCH 12/33] Fix zsh custom completions for empty [String] & String elements. If a Swift custom completion function returns an empty [String], if the user tries to complete it, refuse to complete instead of inserting a blank space into the command line. If a Swift custom completion function returns a [String] including a Swift empty String or including a String with a description but with a blank completion (e.g., ":description"), if that completion is selected, complete to a zsh empty string '' instead of inserting a blank space into the command line. Disambiguating between an empty [String] & a [String] with one empty String element requires that an extra value be appended to the output of the Swift custom function, which is then removed by the completion script. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/CompletionsGenerator.swift | 8 +++ .../Completions/ZshCompletionsGenerator.swift | 10 ++- .../Parsing/CommandParser.swift | 7 ++- .../TestHelpers.swift | 19 +++++- .../MathExampleTests.swift | 62 ++++++++++++++----- .../testMathZshCompletionScript().zsh | 10 ++- .../CompletionScriptTests.swift | 24 +++---- .../Snapshots/testBase_Zsh().zsh | 10 ++- 8 files changed, 112 insertions(+), 38 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index d5db618a7..a8806e63f 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -98,6 +98,14 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { /// /// The environment variable is set in generated completion scripts. static let shellVersionEnvironmentVariableName = "SAP_SHELL_VERSION" + + public func format(completions: [String]) -> String { + var completions = completions + if self == .zsh { + completions.append("END_MARKER") + } + return completions.joined(separator: "\n") + } } struct CompletionsGenerator { diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 7201e8019..ad4bb79a7 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -19,8 +19,14 @@ struct ZshCompletionsGenerator { \(generateCompletionFunction([type]))\ _custom_completion() { - local completions=("${(@f)$($*)}") - _describe '' completions + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + completions=("${completions[@]:0:-1}") + local -ar non_empty_completions=("${completions[@]:#(|:*)}") + local -ar empty_completions=("${(M)completions[@]:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' + fi } \(initialFunctionName) diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index de0ce844a..89aca8db5 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -436,8 +436,11 @@ extension CommandParser { // Parsing and retrieval successful! We don't want to continue with any // other parsing here, so after printing the result of the completion // function, exit with a success code. - let output = completionFunction(completionValues).joined(separator: "\n") - throw ParserError.completionScriptCustomResponse(output) + let completions = completionFunction(completionValues) + throw ParserError.completionScriptCustomResponse( + CompletionShell.requesting?.format(completions: completions) + ?? completions.joined(separator: "\n") + ) } } diff --git a/Sources/ArgumentParserTestHelpers/TestHelpers.swift b/Sources/ArgumentParserTestHelpers/TestHelpers.swift index 8f0dc012f..2df1a88a4 100644 --- a/Sources/ArgumentParserTestHelpers/TestHelpers.swift +++ b/Sources/ArgumentParserTestHelpers/TestHelpers.swift @@ -315,14 +315,17 @@ extension XCTest { expected: String? = nil, exitCode: ExitCode = .success, file: StaticString = #filePath, - line: UInt = #line + line: UInt = #line, + environment: [String: String] = [:] ) throws -> String { try AssertExecuteCommand( command: command.split(separator: " ").map(String.init), expected: expected, exitCode: exitCode, file: file, - line: line) + line: line, + environment: environment + ) } // swift-format-ignore: AlwaysUseLowerCamelCase @@ -332,7 +335,8 @@ extension XCTest { expected: String? = nil, exitCode: ExitCode = .success, file: StaticString = #filePath, - line: UInt = #line + line: UInt = #line, + environment: [String: String] = [:] ) throws -> String { #if os(Windows) throw XCTSkip("Unsupported on this platform") @@ -358,6 +362,15 @@ extension XCTest { let error = Pipe() process.standardError = error + if !environment.isEmpty { + if let existingEnvironment = process.environment { + process.environment = + existingEnvironment.merging(environment) { (_, new) in new } + } else { + process.environment = environment + } + } + guard (try? process.run()) != nil else { XCTFail("Couldn't run command process.", file: file, line: line) return "" diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift index 12273cb79..81ec4907b 100644 --- a/Tests/ArgumentParserExampleTests/MathExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -13,6 +13,12 @@ import ArgumentParser import ArgumentParserTestHelpers import XCTest +#if swift(>=6.0) +@testable internal import struct ArgumentParser.CompletionShell +#else +@testable import struct ArgumentParser.CompletionShell +#endif + final class MathExampleTests: XCTestCase { override func setUp() { #if !os(Windows) && !os(WASI) @@ -219,28 +225,54 @@ extension MathExampleTests { try assertSnapshot(actual: script, extension: "fish") } - func testMath_CustomCompletion() throws { + func testMath_BashCustomCompletion() throws { + try testMath_CustomCompletion(forShell: .bash) + } + + func testMath_FishCustomCompletion() throws { + try testMath_CustomCompletion(forShell: .fish) + } + + func testMath_ZshCustomCompletion() throws { + try testMath_CustomCompletion(forShell: .zsh) + } + + private func testMath_CustomCompletion( + forShell shell: CompletionShell + ) throws { try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom", - expected: """ - hello - helicopter - heliotrope - """) + expected: shell.format(completions: [ + "hello", + "helicopter", + "heliotrope", + ]), + environment: [ + CompletionShell.shellEnvironmentVariableName: shell.rawValue + ] + ) try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom h", - expected: """ - hello - helicopter - heliotrope - """) + expected: shell.format(completions: [ + "hello", + "helicopter", + "heliotrope", + ]), + environment: [ + CompletionShell.shellEnvironmentVariableName: shell.rawValue + ] + ) try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom a", - expected: """ - aardvark - aaaaalbert - """) + expected: shell.format(completions: [ + "aardvark", + "aaaaalbert", + ]), + environment: [ + CompletionShell.shellEnvironmentVariableName: shell.rawValue + ] + ) } } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 25ed62d8c..1f772d15a 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -168,8 +168,14 @@ _math_help() { } _custom_completion() { - local completions=("${(@f)$($*)}") - _describe '' completions + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + completions=("${completions[@]:0:-1}") + local -ar non_empty_completions=("${completions[@]:#(|:*)}") + local -ar empty_completions=("${(M)completions[@]:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\'\'' + fi } _math diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift index 833623894..83bf347af 100644 --- a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift +++ b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift @@ -164,15 +164,15 @@ extension CompletionScriptTests { func assertCustomCompletion( _ arg: String, - shell: String, + shell: CompletionShell, prefix: String = "", file: StaticString = #filePath, line: UInt = #line ) throws { #if !os(Windows) && !os(WASI) do { - setenv("SAP_SHELL", shell, 1) - defer { unsetenv("SAP_SHELL") } + setenv(CompletionShell.shellEnvironmentVariableName, shell.rawValue, 1) + defer { unsetenv(CompletionShell.shellEnvironmentVariableName) } _ = try Custom.parse(["---completion", "--", arg]) XCTFail("Didn't error as expected", file: file, line: line) } catch let error as CommandError { @@ -182,11 +182,11 @@ extension CompletionScriptTests { } AssertEqualStrings( actual: output, - expected: """ - \(prefix)1_\(shell) - \(prefix)2_\(shell) - \(prefix)3_\(shell) - """, + expected: shell.format(completions: [ + "\(prefix)1_\(shell.rawValue)", + "\(prefix)2_\(shell.rawValue)", + "\(prefix)3_\(shell.rawValue)", + ]), file: file, line: line) } @@ -194,7 +194,7 @@ extension CompletionScriptTests { } func assertCustomCompletions( - shell: String, + shell: CompletionShell, file: StaticString = #filePath, line: UInt = #line ) throws { @@ -218,14 +218,14 @@ extension CompletionScriptTests { } func testBashCustomCompletions() throws { - try assertCustomCompletions(shell: "bash") + try assertCustomCompletions(shell: .bash) } func testFishCustomCompletions() throws { - try assertCustomCompletions(shell: "fish") + try assertCustomCompletions(shell: .fish) } func testZshCustomCompletions() throws { - try assertCustomCompletions(shell: "zsh") + try assertCustomCompletions(shell: .zsh) } } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index b78acd869..69fa6da21 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -93,8 +93,14 @@ _base-test_help() { } _custom_completion() { - local completions=("${(@f)$($*)}") - _describe '' completions + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + completions=("${completions[@]:0:-1}") + local -ar non_empty_completions=("${completions[@]:#(|:*)}") + local -ar empty_completions=("${(M)completions[@]:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\'\'' + fi } _base-test From ded075d84e1d5a7585af6429c807c67a43fed5cb Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 21 Jan 2025 06:08:11 -0500 Subject: [PATCH 13/33] Simplify zsh subcommand completion function dispatch. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 25 ++++++++----------- .../testMathZshCompletionScript().zsh | 23 +++-------------- .../Snapshots/testBase_Zsh().zsh | 10 ++------ 3 files changed, 16 insertions(+), 42 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index ad4bb79a7..48a191f0d 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -55,30 +55,25 @@ struct ZshCompletionsGenerator { subcommands.append(HelpCommand.self) } - let subcommandModes = subcommands.map { - """ - '\($0._commandName):\($0.configuration.abstract.zshEscaped())' - """ - } - let subcommandArgs = subcommands.map { - """ - \($0._commandName)) - \(functionName)_\($0._commandName) - ;; - """ - } - subcommandHandler = """ case "${state}" in command) local -ar subcommands=( - \(subcommandModes.joined(separator: "\n")) + \( + subcommands.map { """ + '\($0._commandName):\($0.configuration.abstract.zshEscapingMetacharacters())' + """ + } + .joined(separator: "\n") + ) ) _describe "subcommand" subcommands ;; arg) case "${words[1]}" in - \(subcommandArgs.joined(separator: "\n")) + \(subcommands.map { $0._commandName }.joined(separator: "|"))) + "\(functionName)_${words[1]}" + ;; esac ;; esac diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 1f772d15a..ec28fa2d6 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -32,17 +32,8 @@ _math() { ;; arg) case "${words[1]}" in - add) - _math_add - ;; - multiply) - _math_multiply - ;; - stats) - _math_stats - ;; - help) - _math_help + add|multiply|stats|help) + "_math_${words[1]}" ;; esac ;; @@ -97,14 +88,8 @@ _math_stats() { ;; arg) case "${words[1]}" in - average) - _math_stats_average - ;; - stdev) - _math_stats_stdev - ;; - quantiles) - _math_stats_quantiles + average|stdev|quantiles) + "_math_stats_${words[1]}" ;; esac ;; diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 69fa6da21..231f1591e 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -44,14 +44,8 @@ _base-test() { ;; arg) case "${words[1]}" in - sub-command) - _base-test_sub-command - ;; - escaped-command) - _base-test_escaped-command - ;; - help) - _base-test_help + sub-command|escaped-command|help) + "_base-test_${words[1]}" ;; esac ;; From 28a6b126c97657bf49f9c6a15a10363090f3b82d Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 18 Jan 2025 17:04:48 -0500 Subject: [PATCH 14/33] Restrict access to symbols in ZshCompletionsGenerator.swift. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 48a191f0d..b63890747 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -33,9 +33,9 @@ struct ZshCompletionsGenerator { """ } - static func generateCompletionFunction(_ commands: [ParsableCommand.Type]) - -> String - { + private static func generateCompletionFunction( + _ commands: [ParsableCommand.Type] + ) -> String { guard let type = commands.last else { return "" } let functionName = commands.completionFunctionName() let isRootCommand = commands.count == 1 @@ -118,9 +118,9 @@ struct ZshCompletionsGenerator { .joined() } - static func generateCompletionArguments(_ commands: [ParsableCommand.Type]) - -> [String] - { + private static func generateCompletionArguments( + _ commands: [ParsableCommand.Type] + ) -> [String] { commands .argumentsForHelp(visibility: .default) .compactMap { $0.zshCompletionString(commands) } @@ -144,12 +144,14 @@ extension String { } extension ArgumentDefinition { - var zshCompletionAbstract: String { + private var zshCompletionAbstract: String { guard !help.abstract.isEmpty else { return "" } return "[\(help.abstract.zshEscaped())]" } - func zshCompletionString(_ commands: [ParsableCommand.Type]) -> String? { + fileprivate func zshCompletionString( + _ commands: [ParsableCommand.Type] + ) -> String? { guard help.visibility.base == .default else { return nil } let inputs: String @@ -198,7 +200,7 @@ extension ArgumentDefinition { } /// Returns the zsh "action" for an argument completion string. - func zshActionString(_ commands: [ParsableCommand.Type]) -> String { + private func zshActionString(_ commands: [ParsableCommand.Type]) -> String { switch completion.kind { case .default: return "" From 33fbfbcac7adab877b144548c95a3a9ef7513dd3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 21 Jan 2025 07:03:54 -0500 Subject: [PATCH 15/33] Add default help to zsh completions iff no existing help subcommand. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/CompletionsGenerator.swift | 9 +++++++++ .../Completions/ZshCompletionsGenerator.swift | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index a8806e63f..69af0194b 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -172,6 +172,15 @@ extension ParsableCommand { } } +extension [ParsableCommand.Type] { + // Include default 'help' subcommand in nonempty subcommand list iff no help subcommand already exists. + mutating func addHelpSubcommandIffMissing() { + if !isEmpty && allSatisfy({ $0._commandName != "help" }) { + append(HelpCommand.self) + } + } +} + extension Sequence where Element == ParsableCommand.Type { func completionFunctionName() -> String { "_" diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index b63890747..40ccb1e2d 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -52,7 +52,7 @@ struct ZshCompletionsGenerator { args.append("'(-)*:: :->arg'") if isRootCommand { - subcommands.append(HelpCommand.self) + subcommands.addHelpSubcommandIffMissing() } subcommandHandler = """ From 72eb64c60e4854070524079882989b8033d09bfe Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 21 Jan 2025 08:47:34 -0500 Subject: [PATCH 16/33] Use interpolated Strings in ZshCompletionsGenerator.swift. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/ZshCompletionsGenerator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 40ccb1e2d..128d6c933 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -209,14 +209,14 @@ extension ArgumentDefinition { let pattern = extensions.isEmpty ? "" - : " -g '\(extensions.map { "*." + $0 }.joined(separator: " "))'" + : " -g '\(extensions.map { "*.\($0)" }.joined(separator: " "))'" return "_files\(pattern.zshEscaped())" case .directory: return "_files -/" case .list(let list): - return "(" + list.joined(separator: " ") + ")" + return "(\(list.joined(separator: " ")))" case .shellCommand(let command): return From 83fa27c86d86b6b9f473d1aeb60933c70238e6e3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 21 Jan 2025 09:08:03 -0500 Subject: [PATCH 17/33] Create & use zsh __completion function. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 11 +++++++---- .../Snapshots/testMathZshCompletionScript().zsh | 11 +++++++---- .../Snapshots/testBase_Zsh().zsh | 11 +++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 128d6c933..ed8828be4 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -18,14 +18,17 @@ struct ZshCompletionsGenerator { #compdef \(type._commandName) \(generateCompletionFunction([type]))\ + __completion() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' + } + _custom_completion() { local -a completions completions=("${(@f)"$("${@}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then - completions=("${completions[@]:0:-1}") - local -ar non_empty_completions=("${completions[@]:#(|:*)}") - local -ar empty_completions=("${(M)completions[@]:#(|:*)}") - _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' + __completion "${completions[@]:0:-1}" fi } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index ec28fa2d6..28e9c5bfe 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -152,14 +152,17 @@ _math_help() { return "${ret}" } +__completion() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\'\'' +} + _custom_completion() { local -a completions completions=("${(@f)"$("${@}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then - completions=("${completions[@]:0:-1}") - local -ar non_empty_completions=("${completions[@]:#(|:*)}") - local -ar empty_completions=("${(M)completions[@]:#(|:*)}") - _describe '' non_empty_completions -- empty_completions -P $'\'\'' + __completion "${completions[@]:0:-1}" fi } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 231f1591e..138a02ebb 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -86,14 +86,17 @@ _base-test_help() { return "${ret}" } +__completion() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\'\'' +} + _custom_completion() { local -a completions completions=("${(@f)"$("${@}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then - completions=("${completions[@]:0:-1}") - local -ar non_empty_completions=("${completions[@]:#(|:*)}") - local -ar empty_completions=("${(M)completions[@]:#(|:*)}") - _describe '' non_empty_completions -- empty_completions -P $'\'\'' + __completion "${completions[@]:0:-1}" fi } From 0f8b5ac14e3c0926233a931265a3b9a6cece74a2 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:32:02 -0500 Subject: [PATCH 18/33] Improve zsh escaping. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/CompletionsGenerator.swift | 9 +++++++ .../Completions/ZshCompletionsGenerator.swift | 25 ++++++------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 69af0194b..d500305f3 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -189,3 +189,12 @@ extension Sequence where Element == ParsableCommand.Type { .joined(separator: "_") } } + +extension String { + func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self { + iterationCount == 0 + ? self + : replacingOccurrences(of: "'", with: "'\\''") + .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) + } +} diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index ed8828be4..f90f7ead8 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -64,7 +64,7 @@ struct ZshCompletionsGenerator { local -ar subcommands=( \( subcommands.map { """ - '\($0._commandName):\($0.configuration.abstract.zshEscapingMetacharacters())' + '\($0._commandName):\($0.configuration.abstract.zshEscapeForSingleQuotedExplanation())' """ } .joined(separator: "\n") @@ -131,25 +131,17 @@ struct ZshCompletionsGenerator { } extension String { - fileprivate func zshEscapingSingleQuotes() -> String { - replacingOccurrences(of: "'", with: "'\\''") - } - - fileprivate func zshEscapingMetacharacters() -> String { + fileprivate func zshEscapeForSingleQuotedExplanation() -> String { replacingOccurrences( of: #"[\\\[\]]"#, with: #"\\$0"#, options: .regularExpression - ) - } - - fileprivate func zshEscaped() -> String { - zshEscapingMetacharacters().zshEscapingSingleQuotes() + ).shellEscapeForSingleQuotedString() } } extension ArgumentDefinition { private var zshCompletionAbstract: String { guard !help.abstract.isEmpty else { return "" } - return "[\(help.abstract.zshEscaped())]" + return "[\(help.abstract.zshEscapeForSingleQuotedExplanation())]" } fileprivate func zshCompletionString( @@ -209,11 +201,10 @@ extension ArgumentDefinition { return "" case .file(let extensions): - let pattern = + return extensions.isEmpty - ? "" - : " -g '\(extensions.map { "*.\($0)" }.joined(separator: " "))'" - return "_files\(pattern.zshEscaped())" + ? "_files" + : "_files -g '\\''\(extensions.map { "*.\($0.zshEscapeForSingleQuotedExplanation())" }.joined(separator: " "))'\\''" case .directory: return "_files -/" @@ -223,7 +214,7 @@ extension ArgumentDefinition { case .shellCommand(let command): return - "{local -a list;list=(${(f)\"$(\(command.zshEscapingSingleQuotes()))\"});_describe \"\" list}" + "{local -a list;list=(${(f)\"$(\(command.shellEscapeForSingleQuotedString()))\"});_describe \"\" list}" case .custom: // Generate a call back into the command to retrieve a completions list From d278fd1068b475a5507af4d910d537dc38517e4b Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:53:05 -0500 Subject: [PATCH 19/33] Set zsh settings to a known state. Disable history ! in zsh completion scripts. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/ZshCompletionsGenerator.swift | 4 ++++ .../Snapshots/testMathZshCompletionScript().zsh | 4 ++++ Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index f90f7ead8..8a51c5e35 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -88,6 +88,10 @@ struct ZshCompletionsGenerator { \(functionName)() { \(isRootCommand ? """ + emulate -RL zsh -G + setopt extendedglob + unsetopt aliases banghist + local -xr \(CompletionShell.shellEnvironmentVariableName)=zsh local -x \(CompletionShell.shellVersionEnvironmentVariableName) \(CompletionShell.shellVersionEnvironmentVariableName)="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 28e9c5bfe..edd588cad 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -1,6 +1,10 @@ #compdef math _math() { + emulate -RL zsh -G + setopt extendedglob + unsetopt aliases banghist + local -xr SAP_SHELL=zsh local -x SAP_SHELL_VERSION SAP_SHELL_VERSION="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 138a02ebb..532a77b9b 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -1,6 +1,10 @@ #compdef base-test _base-test() { + emulate -RL zsh -G + setopt extendedglob + unsetopt aliases banghist + local -xr SAP_SHELL=zsh local -x SAP_SHELL_VERSION SAP_SHELL_VERSION="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" From 1d6e52c7697bd158edf10d6af018da7cbf3611bc Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:57:47 -0500 Subject: [PATCH 20/33] Inline single-use functions & variables in ZshCompletionsGenerator.swift. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 68 +++++++------------ 1 file changed, 25 insertions(+), 43 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 8a51c5e35..75a7be4aa 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -12,28 +12,26 @@ struct ZshCompletionsGenerator { /// Generates a Zsh completion script for the given command. static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { - let initialFunctionName = [type].completionFunctionName() - - return """ - #compdef \(type._commandName) - - \(generateCompletionFunction([type]))\ - __completion() { - local -ar non_empty_completions=("${@:#(|:*)}") - local -ar empty_completions=("${(M)@:#(|:*)}") - _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' - } + """ + #compdef \(type._commandName) + + \(generateCompletionFunction([type]))\ + __completion() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' + } - _custom_completion() { - local -a completions - completions=("${(@f)"$("${@}")"}") - if [[ "${#completions[@]}" -gt 1 ]]; then - __completion "${completions[@]:0:-1}" - fi - } + _custom_completion() { + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + __completion "${completions[@]:0:-1}" + fi + } - \(initialFunctionName) - """ + \([type].completionFunctionName()) + """ } private static func generateCompletionFunction( @@ -43,7 +41,8 @@ struct ZshCompletionsGenerator { let functionName = commands.completionFunctionName() let isRootCommand = commands.count == 1 - var args = generateCompletionArguments(commands) + var args = commands.argumentsForHelp(visibility: .default) + .compactMap { $0.zshCompletionString(commands) } var subcommands = type.configuration.subcommands .filter { $0.configuration.shouldDisplay } @@ -84,7 +83,7 @@ struct ZshCompletionsGenerator { """ } - let functionText = """ + return """ \(functionName)() { \(isRootCommand ? """ @@ -116,21 +115,8 @@ struct ZshCompletionsGenerator { return "${ret}" } - + \(subcommands.map { generateCompletionFunction(commands + [$0]) }.joined()) """ - - return functionText - + subcommands - .map { generateCompletionFunction(commands + [$0]) } - .joined() - } - - private static func generateCompletionArguments( - _ commands: [ParsableCommand.Type] - ) -> [String] { - commands - .argumentsForHelp(visibility: .default) - .compactMap { $0.zshCompletionString(commands) } } } @@ -166,16 +152,12 @@ extension ArgumentDefinition { case 0: line = "" case 1: - let star = isRepeatableOption ? "*" : "" - line = """ - \(star)\(names[0].synopsisString)\(zshCompletionAbstract) - """ + line = + "\(isRepeatableOption ? "*" : "")\(names[0].synopsisString)\(zshCompletionAbstract)" default: let synopses = names.map { $0.synopsisString } - let suppression = - isRepeatableOption ? "*" : "(\(synopses.joined(separator: " ")))" line = """ - \(suppression)'\ + \(isRepeatableOption ? "*" : "(\(synopses.joined(separator: " ")))")'\ {\(synopses.joined(separator: ","))}\ '\(zshCompletionAbstract) """ From fc8b1d5423e17a8ce4332532eedff8ba1ad8f3a9 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:03:03 -0500 Subject: [PATCH 21/33] Overhaul ZshCompletionsGenerator.swift as [ParsableCommand.Type] extension. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/CompletionsGenerator.swift | 2 +- .../Completions/ZshCompletionsGenerator.swift | 26 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index d500305f3..2fd3abdaf 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -137,7 +137,7 @@ struct CompletionsGenerator { CompletionShell._requesting.withLock { $0 = shell } switch shell { case .zsh: - return ZshCompletionsGenerator.generateCompletionScript(command) + return [command].zshCompletionScript case .bash: return BashCompletionsGenerator.generateCompletionScript(command) case .fish: diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 75a7be4aa..7612d2ffd 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -9,13 +9,13 @@ // //===----------------------------------------------------------------------===// -struct ZshCompletionsGenerator { +extension [ParsableCommand.Type] { /// Generates a Zsh completion script for the given command. - static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { + var zshCompletionScript: String { """ - #compdef \(type._commandName) + #compdef \(first?._commandName ?? "") - \(generateCompletionFunction([type]))\ + \(completionFunctions)\ __completion() { local -ar non_empty_completions=("${@:#(|:*)}") local -ar empty_completions=("${(M)@:#(|:*)}") @@ -30,19 +30,17 @@ struct ZshCompletionsGenerator { fi } - \([type].completionFunctionName()) + \(completionFunctionName()) """ } - private static func generateCompletionFunction( - _ commands: [ParsableCommand.Type] - ) -> String { - guard let type = commands.last else { return "" } - let functionName = commands.completionFunctionName() - let isRootCommand = commands.count == 1 + private var completionFunctions: String { + guard let type = last else { return "" } + let functionName = completionFunctionName() + let isRootCommand = count == 1 - var args = commands.argumentsForHelp(visibility: .default) - .compactMap { $0.zshCompletionString(commands) } + var args = argumentsForHelp(visibility: .default) + .compactMap { $0.zshCompletionString(self) } var subcommands = type.configuration.subcommands .filter { $0.configuration.shouldDisplay } @@ -115,7 +113,7 @@ struct ZshCompletionsGenerator { return "${ret}" } - \(subcommands.map { generateCompletionFunction(commands + [$0]) }.joined()) + \(subcommands.map { (self + [$0]).completionFunctions }.joined()) """ } } From afecd9cc11b46c1235af2fdab1fe0c8f7e86ffdb Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:14:11 -0500 Subject: [PATCH 22/33] Move functions in ZshCompletionsGenerator.swift. Move from ArgumentDefinition extension to [ParsableCommand.Type] extension. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 92 ++++++++++--------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 7612d2ffd..27e83166d 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -40,7 +40,7 @@ extension [ParsableCommand.Type] { let isRootCommand = count == 1 var args = argumentsForHelp(visibility: .default) - .compactMap { $0.zshCompletionString(self) } + .compactMap { zshCompletionString($0) } var subcommands = type.configuration.subcommands .filter { $0.configuration.shouldDisplay } @@ -116,71 +116,41 @@ extension [ParsableCommand.Type] { \(subcommands.map { (self + [$0]).completionFunctions }.joined()) """ } -} -extension String { - fileprivate func zshEscapeForSingleQuotedExplanation() -> String { - replacingOccurrences( - of: #"[\\\[\]]"#, with: #"\\$0"#, options: .regularExpression - ).shellEscapeForSingleQuotedString() - } -} - -extension ArgumentDefinition { - private var zshCompletionAbstract: String { - guard !help.abstract.isEmpty else { return "" } - return "[\(help.abstract.zshEscapeForSingleQuotedExplanation())]" - } - - fileprivate func zshCompletionString( - _ commands: [ParsableCommand.Type] - ) -> String? { - guard help.visibility.base == .default else { return nil } + private func zshCompletionString(_ arg: ArgumentDefinition) -> String? { + guard arg.help.visibility.base == .default else { return nil } let inputs: String - switch update { + switch arg.update { case .unary: - inputs = ":\(valueName):\(zshActionString(commands))" + inputs = ":\(arg.valueName):\(zshActionString(arg))" case .nullary: inputs = "" } let line: String - switch names.count { + switch arg.names.count { case 0: line = "" case 1: - line = - "\(isRepeatableOption ? "*" : "")\(names[0].synopsisString)\(zshCompletionAbstract)" + line = """ + \(arg.isRepeatableOption ? "*" : "")\(arg.names[0].synopsisString)\(arg.zshCompletionAbstract) + """ default: - let synopses = names.map { $0.synopsisString } + let synopses = arg.names.map { $0.synopsisString } line = """ - \(isRepeatableOption ? "*" : "(\(synopses.joined(separator: " ")))")'\ + \(arg.isRepeatableOption ? "*" : "(\(synopses.joined(separator: " ")))")'\ {\(synopses.joined(separator: ","))}\ - '\(zshCompletionAbstract) + '\(arg.zshCompletionAbstract) """ } return "'\(line)\(inputs)'" } - /// - returns: `true` if `self` is an option and can be tab-completed multiple times in one command line. - /// For example, `ssh` allows the `-L` option to be given multiple times, to establish multiple port forwardings. - private var isRepeatableOption: Bool { - guard - case .named(_) = kind, - help.options.contains(.isRepeating) - else { return false } - - switch parsingStrategy { - case .default, .unconditional: return true - default: return false - } - } - /// Returns the zsh "action" for an argument completion string. - private func zshActionString(_ commands: [ParsableCommand.Type]) -> String { - switch completion.kind { + private func zshActionString(_ arg: ArgumentDefinition) -> String { + switch arg.completion.kind { case .default: return "" @@ -203,7 +173,39 @@ extension ArgumentDefinition { case .custom: // Generate a call back into the command to retrieve a completions list return - "{_custom_completion \"${command_name}\" \(customCompletionCall(commands)) \"${command_line[@]}\"}" + "{_custom_completion \"${command_name}\" \(arg.customCompletionCall(self)) \"${command_line[@]}\"}" + } + } +} + +extension String { + fileprivate func zshEscapeForSingleQuotedExplanation() -> String { + replacingOccurrences( + of: #"[\\\[\]]"#, + with: #"\\$0"#, + options: .regularExpression + ) + .shellEscapeForSingleQuotedString() + } +} + +extension ArgumentDefinition { + /// - returns: `true` if `self` is an option and can be tab-completed multiple times in one command line. + /// For example, `ssh` allows the `-L` option to be given multiple times, to establish multiple port forwardings. + fileprivate var isRepeatableOption: Bool { + guard + case .named(_) = kind, + help.options.contains(.isRepeating) + else { return false } + + switch parsingStrategy { + case .default, .unconditional: return true + default: return false } } + + fileprivate var zshCompletionAbstract: String { + guard !help.abstract.isEmpty else { return "" } + return "[\(help.abstract.zshEscapeForSingleQuotedExplanation())]" + } } From a2743b13cf56043abbda88c582c16fae85701213 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:18:36 -0500 Subject: [PATCH 23/33] Move zsh helper functions before command functions to mirror other shells. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 2 +- .../testMathZshCompletionScript().zsh | 28 +++++++++---------- .../Snapshots/testBase_Zsh().zsh | 28 +++++++++---------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 27e83166d..58024746c 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -15,7 +15,6 @@ extension [ParsableCommand.Type] { """ #compdef \(first?._commandName ?? "") - \(completionFunctions)\ __completion() { local -ar non_empty_completions=("${@:#(|:*)}") local -ar empty_completions=("${(M)@:#(|:*)}") @@ -30,6 +29,7 @@ extension [ParsableCommand.Type] { fi } + \(completionFunctions)\ \(completionFunctionName()) """ } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index edd588cad..d72bf5cba 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -1,5 +1,19 @@ #compdef math +__completion() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\'\'' +} + +_custom_completion() { + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + __completion "${completions[@]:0:-1}" + fi +} + _math() { emulate -RL zsh -G setopt extendedglob @@ -156,18 +170,4 @@ _math_help() { return "${ret}" } -__completion() { - local -ar non_empty_completions=("${@:#(|:*)}") - local -ar empty_completions=("${(M)@:#(|:*)}") - _describe '' non_empty_completions -- empty_completions -P $'\'\'' -} - -_custom_completion() { - local -a completions - completions=("${(@f)"$("${@}")"}") - if [[ "${#completions[@]}" -gt 1 ]]; then - __completion "${completions[@]:0:-1}" - fi -} - _math diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 532a77b9b..7a5989bc8 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -1,5 +1,19 @@ #compdef base-test +__completion() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\'\'' +} + +_custom_completion() { + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + __completion "${completions[@]:0:-1}" + fi +} + _base-test() { emulate -RL zsh -G setopt extendedglob @@ -90,18 +104,4 @@ _base-test_help() { return "${ret}" } -__completion() { - local -ar non_empty_completions=("${@:#(|:*)}") - local -ar empty_completions=("${(M)@:#(|:*)}") - _describe '' non_empty_completions -- empty_completions -P $'\'\'' -} - -_custom_completion() { - local -a completions - completions=("${(@f)"$("${@}")"}") - if [[ "${#completions[@]}" -gt 1 ]]; then - __completion "${completions[@]:0:-1}" - fi -} - _base-test From ea62664025bf65ac53b541a6d4f3ab976fbc0bb4 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Tue, 28 Jan 2025 19:41:15 -0500 Subject: [PATCH 24/33] Prefix zsh helper functions with command name to prevent naming clashes. Function names are globally scoped. Without namespacing, if 2 programs use different versions of Swift Argument Parser, one could overwrite the other's different version of the same helper function. Renamed functions from *_completion to *_complete, as they complete, not return a completion. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 18 +++++++++++++----- .../testMathZshCompletionScript().zsh | 14 +++++++------- .../Snapshots/testBase_Zsh().zsh | 18 +++++++++--------- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 58024746c..826044342 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -15,17 +15,17 @@ extension [ParsableCommand.Type] { """ #compdef \(first?._commandName ?? "") - __completion() { + \(completeFunctionName)() { local -ar non_empty_completions=("${@:#(|:*)}") local -ar empty_completions=("${(M)@:#(|:*)}") _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' } - _custom_completion() { + \(customCompleteFunctionName)() { local -a completions completions=("${(@f)"$("${@}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then - __completion "${completions[@]:0:-1}" + \(completeFunctionName) "${completions[@]:0:-1}" fi } @@ -164,7 +164,7 @@ extension [ParsableCommand.Type] { return "_files -/" case .list(let list): - return "(\(list.joined(separator: " ")))" + return "{\(completeFunctionName) \(list.joined(separator: " "))}" case .shellCommand(let command): return @@ -173,9 +173,17 @@ extension [ParsableCommand.Type] { case .custom: // Generate a call back into the command to retrieve a completions list return - "{_custom_completion \"${command_name}\" \(arg.customCompletionCall(self)) \"${command_line[@]}\"}" + "{\(customCompleteFunctionName) \"${command_name}\" \(arg.customCompletionCall(self)) \"${command_line[@]}\"}" } } + + private var completeFunctionName: String { + "__\(first?._commandName ?? "")_complete" + } + + private var customCompleteFunctionName: String { + "__\(first?._commandName ?? "")_custom_complete" + } } extension String { diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index d72bf5cba..81bab606e 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -1,16 +1,16 @@ #compdef math -__completion() { +__math_complete() { local -ar non_empty_completions=("${@:#(|:*)}") local -ar empty_completions=("${(M)@:#(|:*)}") _describe '' non_empty_completions -- empty_completions -P $'\'\'' } -_custom_completion() { +__math_custom_complete() { local -a completions completions=("${(@f)"$("${@}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then - __completion "${completions[@]:0:-1}" + __math_complete "${completions[@]:0:-1}" fi } @@ -119,7 +119,7 @@ _math_stats() { _math_stats_average() { local -i ret=1 local -ar args=( - '--kind[The kind of average to provide.]:kind:(mean median mode)' + '--kind[The kind of average to provide.]:kind:{__math_complete mean median mode}' ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' @@ -144,13 +144,13 @@ _math_stats_stdev() { _math_stats_quantiles() { local -i ret=1 local -ar args=( - ':one-of-four:(alphabet alligator branch braggart)' - ':custom-arg:{_custom_completion "${command_name}" ---completion stats quantiles -- customArg "${command_line[@]}"}' + ':one-of-four:{__math_complete alphabet alligator branch braggart}' + ':custom-arg:{__math_custom_complete "${command_name}" ---completion stats quantiles -- customArg "${command_line[@]}"}' ':values:' '--file:file:_files -g '\''*.txt *.md'\''' '--directory:directory:_files -/' '--shell:shell:{local -a list;list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"});_describe "" list}' - '--custom:custom:{_custom_completion "${command_name}" ---completion stats quantiles -- --custom "${command_line[@]}"}' + '--custom:custom:{__math_custom_complete "${command_name}" ---completion stats quantiles -- --custom "${command_line[@]}"}' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 7a5989bc8..d4db6c390 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -1,16 +1,16 @@ #compdef base-test -__completion() { +__base-test_complete() { local -ar non_empty_completions=("${@:#(|:*)}") local -ar empty_completions=("${(M)@:#(|:*)}") _describe '' non_empty_completions -- empty_completions -P $'\'\'' } -_custom_completion() { +__base-test_custom_complete() { local -a completions completions=("${(@f)"$("${@}")"}") if [[ "${#completions[@]}" -gt 1 ]]; then - __completion "${completions[@]:0:-1}" + __base-test_complete "${completions[@]:0:-1}" fi } @@ -33,19 +33,19 @@ _base-test() { local -i ret=1 local -ar args=( '--name[The user'\''s name.]:name:' - '--kind:kind:(one two custom-three)' - '--other-kind:other-kind:(b1_zsh b2_zsh b3_zsh)' + '--kind:kind:{__base-test_complete one two custom-three}' + '--other-kind:other-kind:{__base-test_complete b1_zsh b2_zsh b3_zsh}' '--path1:path1:_files' '--path2:path2:_files' - '--path3:path3:(c1_zsh c2_zsh c3_zsh)' + '--path3:path3:{__base-test_complete c1_zsh c2_zsh c3_zsh}' '--one' '--two' '--three' '*--kind-counter' '*--rep1:rep1:' '*'{-r,--rep2}':rep2:' - ':argument:{_custom_completion "${command_name}" ---completion -- argument "${command_line[@]}"}' - ':nested-argument:{_custom_completion "${command_name}" ---completion -- nested.nestedArgument "${command_line[@]}"}' + ':argument:{__base-test_custom_complete "${command_name}" ---completion -- argument "${command_line[@]}"}' + ':nested-argument:{__base-test_custom_complete "${command_name}" ---completion -- nested.nestedArgument "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' @@ -86,7 +86,7 @@ _base-test_escaped-command() { local -i ret=1 local -ar args=( '--one[Escaped chars: '\''\[\]\\.]:one:' - ':two:{_custom_completion "${command_name}" ---completion escaped-command -- two "${command_line[@]}"}' + ':two:{__base-test_custom_complete "${command_name}" ---completion escaped-command -- two "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' ) _arguments -w -s -S "${args[@]}" && ret=0 From 15bbfa032c28711dcd9132c566836479cf62696c Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:08:30 -0500 Subject: [PATCH 25/33] Separate zsh _arguments flags from specs using :. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 2 +- .../Snapshots/testMathZshCompletionScript().zsh | 16 ++++++++-------- .../Snapshots/testBase_Zsh().zsh | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 826044342..22095a43e 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -108,7 +108,7 @@ extension [ParsableCommand.Type] { local -ar args=( \(args.joined(separator: "\n").indentingEachLine(by: 8)) ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 \(subcommandHandler) return "${ret}" } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 81bab606e..20e5857b9 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -37,7 +37,7 @@ _math() { '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 case "${state}" in command) local -ar subcommands=( @@ -68,7 +68,7 @@ _math_add() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } @@ -81,7 +81,7 @@ _math_multiply() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } @@ -94,7 +94,7 @@ _math_stats() { '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 case "${state}" in command) local -ar subcommands=( @@ -124,7 +124,7 @@ _math_stats_average() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } @@ -136,7 +136,7 @@ _math_stats_stdev() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } @@ -154,7 +154,7 @@ _math_stats_quantiles() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } @@ -165,7 +165,7 @@ _math_help() { ':subcommands:' '--version[Show the version.]' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index d4db6c390..7d5167a28 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -50,7 +50,7 @@ _base-test() { '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 case "${state}" in command) local -ar subcommands=( @@ -77,7 +77,7 @@ _base-test_sub-command() { local -ar args=( '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } @@ -89,7 +89,7 @@ _base-test_escaped-command() { ':two:{__base-test_custom_complete "${command_name}" ---completion escaped-command -- two "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } @@ -99,7 +99,7 @@ _base-test_help() { local -ar args=( ':subcommands:' ) - _arguments -w -s -S "${args[@]}" && ret=0 + _arguments -w -s -S : "${args[@]}" && ret=0 return "${ret}" } From f87085cd2cb689f5f8ebec102e45d7e5a23209d1 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 30 Jan 2025 08:53:27 -0500 Subject: [PATCH 26/33] Rename zsh args variable as arg_specs. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 12 +++---- .../testMathZshCompletionScript().zsh | 32 +++++++++---------- .../Snapshots/testBase_Zsh().zsh | 16 +++++----- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 22095a43e..896315cd9 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -39,7 +39,7 @@ extension [ParsableCommand.Type] { let functionName = completionFunctionName() let isRootCommand = count == 1 - var args = argumentsForHelp(visibility: .default) + var argumentSpecs = argumentsForHelp(visibility: .default) .compactMap { zshCompletionString($0) } var subcommands = type.configuration.subcommands .filter { $0.configuration.shouldDisplay } @@ -48,8 +48,8 @@ extension [ParsableCommand.Type] { if subcommands.isEmpty { subcommandHandler = "" } else { - args.append("'(-): :->command'") - args.append("'(-)*:: :->arg'") + argumentSpecs.append("'(-): :->command'") + argumentSpecs.append("'(-)*:: :->arg'") if isRootCommand { subcommands.addHelpSubcommandIffMissing() @@ -105,10 +105,10 @@ extension [ParsableCommand.Type] { : "" )\ local -i ret=1 - local -ar args=( - \(args.joined(separator: "\n").indentingEachLine(by: 8)) + local -ar arg_specs=( + \(argumentSpecs.joined(separator: "\n").indentingEachLine(by: 8)) ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 \(subcommandHandler) return "${ret}" } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 20e5857b9..95e9f08ab 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -31,13 +31,13 @@ _math() { local -ar command_line=("${words[@]}") local -i ret=1 - local -ar args=( + local -ar arg_specs=( '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 case "${state}" in command) local -ar subcommands=( @@ -62,39 +62,39 @@ _math() { _math_add() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } _math_multiply() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } _math_stats() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 case "${state}" in command) local -ar subcommands=( @@ -118,32 +118,32 @@ _math_stats() { _math_stats_average() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( '--kind[The kind of average to provide.]:kind:{__math_complete mean median mode}' ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } _math_stats_stdev() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } _math_stats_quantiles() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( ':one-of-four:{__math_complete alphabet alligator branch braggart}' ':custom-arg:{__math_custom_complete "${command_name}" ---completion stats quantiles -- customArg "${command_line[@]}"}' ':values:' @@ -154,18 +154,18 @@ _math_stats_quantiles() { '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } _math_help() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( ':subcommands:' '--version[Show the version.]' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 7d5167a28..767a50cd7 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -31,7 +31,7 @@ _base-test() { local -ar command_line=("${words[@]}") local -i ret=1 - local -ar args=( + local -ar arg_specs=( '--name[The user'\''s name.]:name:' '--kind:kind:{__base-test_complete one two custom-three}' '--other-kind:other-kind:{__base-test_complete b1_zsh b2_zsh b3_zsh}' @@ -50,7 +50,7 @@ _base-test() { '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 case "${state}" in command) local -ar subcommands=( @@ -74,32 +74,32 @@ _base-test() { _base-test_sub-command() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } _base-test_escaped-command() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( '--one[Escaped chars: '\''\[\]\\.]:one:' ':two:{__base-test_custom_complete "${command_name}" ---completion escaped-command -- two "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } _base-test_help() { local -i ret=1 - local -ar args=( + local -ar arg_specs=( ':subcommands:' ) - _arguments -w -s -S : "${args[@]}" && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 return "${ret}" } From 697e5e8cc6573c301d6fd74749b0e964a8de0fd6 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 30 Jan 2025 08:56:13 -0500 Subject: [PATCH 27/33] =?UTF-8?q?Simplify=20zshCompletionString(=E2=80=A6)?= =?UTF-8?q?.?= 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/ZshCompletionsGenerator.swift | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 896315cd9..8816d67ae 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -120,14 +120,6 @@ extension [ParsableCommand.Type] { private func zshCompletionString(_ arg: ArgumentDefinition) -> String? { guard arg.help.visibility.base == .default else { return nil } - let inputs: String - switch arg.update { - case .unary: - inputs = ":\(arg.valueName):\(zshActionString(arg))" - case .nullary: - inputs = "" - } - let line: String switch arg.names.count { case 0: @@ -145,7 +137,12 @@ extension [ParsableCommand.Type] { """ } - return "'\(line)\(inputs)'" + switch arg.update { + case .unary: + return "'\(line):\(arg.valueName):\(zshActionString(arg))'" + case .nullary: + return "'\(line)'" + } } /// Returns the zsh "action" for an argument completion string. From c6d137530a7bbd06be3a3a25fa4c7bfbd09369fc Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:14:21 -0500 Subject: [PATCH 28/33] Allow generating zsh setup scripts for arguments. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 8816d67ae..56fb58919 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -39,8 +39,11 @@ extension [ParsableCommand.Type] { let functionName = completionFunctionName() let isRootCommand = count == 1 - var argumentSpecs = argumentsForHelp(visibility: .default) - .compactMap { zshCompletionString($0) } + let argumentSpecsAndSetupScripts = argumentsForHelp(visibility: .default) + .compactMap { argumentSpecAndSetupScript($0) } + var argumentSpecs = argumentSpecsAndSetupScripts.map(\.argumentSpec) + let setupScripts = argumentSpecsAndSetupScripts.compactMap(\.setupScript) + var subcommands = type.configuration.subcommands .filter { $0.configuration.shouldDisplay } @@ -105,6 +108,7 @@ extension [ParsableCommand.Type] { : "" )\ local -i ret=1 + \(setupScripts.map { "\($0)\n" }.joined().indentingEachLine(by: 4))\ local -ar arg_specs=( \(argumentSpecs.joined(separator: "\n").indentingEachLine(by: 8)) ) @@ -117,7 +121,9 @@ extension [ParsableCommand.Type] { """ } - private func zshCompletionString(_ arg: ArgumentDefinition) -> String? { + private func argumentSpecAndSetupScript( + _ arg: ArgumentDefinition + ) -> (argumentSpec: String, setupScript: String?)? { guard arg.help.visibility.base == .default else { return nil } let line: String @@ -139,38 +145,47 @@ extension [ParsableCommand.Type] { switch arg.update { case .unary: - return "'\(line):\(arg.valueName):\(zshActionString(arg))'" + let (argumentAction, setupScript) = argumentActionAndSetupScript(arg) + return ("'\(line):\(arg.valueName):\(argumentAction)'", setupScript) case .nullary: - return "'\(line)'" + return ("'\(line)'", nil) } } /// Returns the zsh "action" for an argument completion string. - private func zshActionString(_ arg: ArgumentDefinition) -> String { + private func argumentActionAndSetupScript( + _ arg: ArgumentDefinition + ) -> (argumentAction: String, setupScript: String?) { switch arg.completion.kind { case .default: - return "" + return ("", nil) case .file(let extensions): return extensions.isEmpty - ? "_files" - : "_files -g '\\''\(extensions.map { "*.\($0.zshEscapeForSingleQuotedExplanation())" }.joined(separator: " "))'\\''" + ? ("_files", nil) + : ( + "_files -g '\\''\(extensions.map { "*.\($0.zshEscapeForSingleQuotedExplanation())" }.joined(separator: " "))'\\''", + nil + ) case .directory: - return "_files -/" + return ("_files -/", nil) case .list(let list): - return "{\(completeFunctionName) \(list.joined(separator: " "))}" + return ("{\(completeFunctionName) \(list.joined(separator: " "))}", nil) case .shellCommand(let command): - return - "{local -a list;list=(${(f)\"$(\(command.shellEscapeForSingleQuotedString()))\"});_describe \"\" list}" + return ( + "{local -a list;list=(${(f)\"$(\(command.shellEscapeForSingleQuotedString()))\"});_describe \"\" list}", + nil + ) case .custom: - // Generate a call back into the command to retrieve a completions list - return - "{\(customCompleteFunctionName) \"${command_name}\" \(arg.customCompletionCall(self)) \"${command_line[@]}\"}" + return ( + "{\(customCompleteFunctionName) \"${command_name}\" \(arg.customCompletionCall(self)) \"${command_line[@]}\"}", + nil + ) } } From e8ca77d29a7cceb1725e9e5a15f92e8508b9bbf7 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:55:43 -0500 Subject: [PATCH 29/33] Use zsh array for list completions instead of nested strings. Allows list completions to contain spaces. Resolve #726 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/CompletionsGenerator.swift | 10 ++++++++++ .../Completions/ZshCompletionsGenerator.swift | 15 ++++++++++++++- .../Snapshots/testMathZshCompletionScript().zsh | 6 ++++-- .../Snapshots/testBase_Zsh().zsh | 9 ++++++--- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 2fd3abdaf..d8998836b 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -188,6 +188,12 @@ extension Sequence where Element == ParsableCommand.Type { .uniquingAdjacentElements() .joined(separator: "_") } + + var shellVariableNamePrefix: String { + flatMap { $0.compositeCommandName } + .joined(separator: "_") + .shellEscapeForVariableName() + } } extension String { @@ -197,4 +203,8 @@ extension String { : replacingOccurrences(of: "'", with: "'\\''") .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) } + + func shellEscapeForVariableName() -> Self { + replacingOccurrences(of: "-", with: "_") + } } diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 56fb58919..1b71af93a 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -173,7 +173,11 @@ extension [ParsableCommand.Type] { return ("_files -/", nil) case .list(let list): - return ("{\(completeFunctionName) \(list.joined(separator: " "))}", nil) + let variableName = variableName(arg) + return ( + "{\(completeFunctionName) \"${\(variableName)[@]}\"}", + "local -ar \(variableName)=(\(list.map { "'\($0.shellEscapeForSingleQuotedString())'" }.joined(separator: " ")))" + ) case .shellCommand(let command): return ( @@ -189,6 +193,15 @@ extension [ParsableCommand.Type] { } } + private func variableName(_ arg: ArgumentDefinition) -> String { + guard let argName = arg.names.preferredName else { + return + "\(shellVariableNamePrefix)_\(arg.valueName.shellEscapeForVariableName())" + } + return + "\(argName.case == .long ? "__" : "_")\(shellVariableNamePrefix)_\(argName.valueString.shellEscapeForVariableName())" + } + private var completeFunctionName: String { "__\(first?._commandName ?? "")_complete" } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index 95e9f08ab..cf7c3695a 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -118,8 +118,9 @@ _math_stats() { _math_stats_average() { local -i ret=1 + local -ar __math_stats_average_kind=('mean' 'median' 'mode') local -ar arg_specs=( - '--kind[The kind of average to provide.]:kind:{__math_complete mean median mode}' + '--kind[The kind of average to provide.]:kind:{__math_complete "${__math_stats_average_kind[@]}"}' ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' @@ -143,8 +144,9 @@ _math_stats_stdev() { _math_stats_quantiles() { local -i ret=1 + local -ar math_stats_quantiles_one_of_four=('alphabet' 'alligator' 'branch' 'braggart') local -ar arg_specs=( - ':one-of-four:{__math_complete alphabet alligator branch braggart}' + ':one-of-four:{__math_complete "${math_stats_quantiles_one_of_four[@]}"}' ':custom-arg:{__math_custom_complete "${command_name}" ---completion stats quantiles -- customArg "${command_line[@]}"}' ':values:' '--file:file:_files -g '\''*.txt *.md'\''' diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 767a50cd7..1894128ef 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -31,13 +31,16 @@ _base-test() { local -ar command_line=("${words[@]}") local -i ret=1 + local -ar __base_test_kind=('one' 'two' 'custom-three') + local -ar __base_test_other_kind=('b1_zsh' 'b2_zsh' 'b3_zsh') + local -ar __base_test_path3=('c1_zsh' 'c2_zsh' 'c3_zsh') local -ar arg_specs=( '--name[The user'\''s name.]:name:' - '--kind:kind:{__base-test_complete one two custom-three}' - '--other-kind:other-kind:{__base-test_complete b1_zsh b2_zsh b3_zsh}' + '--kind:kind:{__base-test_complete "${__base_test_kind[@]}"}' + '--other-kind:other-kind:{__base-test_complete "${__base_test_other_kind[@]}"}' '--path1:path1:_files' '--path2:path2:_files' - '--path3:path3:{__base-test_complete c1_zsh c2_zsh c3_zsh}' + '--path3:path3:{__base-test_complete "${__base_test_path3[@]}"}' '--one' '--two' '--three' From 8583ab6223bc7a6b2ba6bf4872b73d60cf7646c5 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:13:51 -0500 Subject: [PATCH 30/33] =?UTF-8?q?Make=20CompletionShell.format(=E2=80=A6)?= =?UTF-8?q?=20internal=20instead=20of=20public.?= 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> --- 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 d8998836b..fa2b278e3 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -99,7 +99,7 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { /// The environment variable is set in generated completion scripts. static let shellVersionEnvironmentVariableName = "SAP_SHELL_VERSION" - public func format(completions: [String]) -> String { + func format(completions: [String]) -> String { var completions = completions if self == .zsh { completions.append("END_MARKER") From ecb43b9efbb2ca82eb50e981211889a76278a5e3 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:15:44 -0500 Subject: [PATCH 31/33] Reword uses of "iff" in completions code. Redid a comment as a DocC. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../ArgumentParser/Completions/CompletionsGenerator.swift | 5 +++-- .../ArgumentParser/Completions/ZshCompletionsGenerator.swift | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index fa2b278e3..d0a1b4110 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -173,8 +173,9 @@ extension ParsableCommand { } extension [ParsableCommand.Type] { - // Include default 'help' subcommand in nonempty subcommand list iff no help subcommand already exists. - mutating func addHelpSubcommandIffMissing() { + /// 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" }) { append(HelpCommand.self) } diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 1b71af93a..9232fd30c 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -55,7 +55,7 @@ extension [ParsableCommand.Type] { argumentSpecs.append("'(-)*:: :->arg'") if isRootCommand { - subcommands.addHelpSubcommandIffMissing() + subcommands.addHelpSubcommandIfMissing() } subcommandHandler = """ From 19e4df1356e4d650a09555e0657812c80ab8f7e7 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 13 Feb 2025 20:05:47 -0500 Subject: [PATCH 32/33] Replace zsh END_MARKER pseudo-completion with a space to ease migration. Document why & how this pseudo-completion is used. Do not trim whitespace in testing, as that breaks with the space pseudo-completion. Testing should be as exact as possible; trimming whitespace makes it less exact. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/CompletionsGenerator.swift | 5 +++- .../TestHelpers.swift | 8 +++--- .../CountLinesExampleTests.swift | 6 +++-- .../MathExampleTests.swift | 26 +++++++++++++------ .../RepeatExampleTests.swift | 10 +++++++ .../RollDiceExampleTests.swift | 4 +++ .../Snapshots/testColorDoccReference().md | 5 ++++ .../testCountLinesDoccReference().md | 5 ++++ .../Snapshots/testMathDoccReference().md | 5 ++++ .../Snapshots/testRepeatDoccReference().md | 5 ++++ .../Snapshots/testRollDoccReference().md | 5 ++++ .../Snapshots/testADumpHelp().json | 2 +- .../Snapshots/testBDumpHelp().json | 2 +- .../Snapshots/testBase_Bash().bash | 2 +- .../Snapshots/testBase_Fish().fish | 2 +- .../Snapshots/testBase_Zsh().zsh | 2 +- .../Snapshots/testCDumpHelp().json | 2 +- 17 files changed, 74 insertions(+), 22 deletions(-) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index d0a1b4110..f725dd9d0 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -102,7 +102,10 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { func format(completions: [String]) -> String { var completions = completions if self == .zsh { - completions.append("END_MARKER") + // This pseudo-completion is removed by the zsh completion script. + // It allows trailing empty string completions to work in zsh. + // zsh completion scripts generated by older SAP versions ignore spaces. + completions.append(" ") } return completions.joined(separator: "\n") } diff --git a/Sources/ArgumentParserTestHelpers/TestHelpers.swift b/Sources/ArgumentParserTestHelpers/TestHelpers.swift index 2df1a88a4..507ab2de6 100644 --- a/Sources/ArgumentParserTestHelpers/TestHelpers.swift +++ b/Sources/ArgumentParserTestHelpers/TestHelpers.swift @@ -379,11 +379,9 @@ extension XCTest { let outputData = output.fileHandleForReading.readDataToEndOfFile() let outputActual = String(data: outputData, encoding: .utf8)! - .trimmingCharacters(in: .whitespacesAndNewlines) let errorData = error.fileHandleForReading.readDataToEndOfFile() let errorActual = String(data: errorData, encoding: .utf8)! - .trimmingCharacters(in: .whitespacesAndNewlines) if let expected = expected { AssertEqualStrings( @@ -407,8 +405,8 @@ extension XCTest { file: StaticString = #filePath, line: UInt = #line ) throws { AssertEqualStrings( - actual: actual.trimmingCharacters(in: .whitespacesAndNewlines), - expected: expected.trimmingCharacters(in: .whitespacesAndNewlines), + actual: actual, + expected: expected, file: file, line: line) @@ -463,7 +461,7 @@ extension XCTest { let expected = try String(contentsOf: snapshotFileURL, encoding: .utf8) AssertEqualStrings( actual: actual, - expected: expected.trimmingCharacters(in: .newlines), + expected: expected, file: file, line: line) return expected diff --git a/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift b/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift index eb4758c3f..424b56f27 100644 --- a/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift @@ -26,9 +26,9 @@ final class CountLinesExampleTests: XCTestCase { let testFile = try XCTUnwrap( Bundle.module.url(forResource: "CountLinesTest", withExtension: "txt")) try AssertExecuteCommand( - command: "count-lines \(testFile.path)", expected: "20") + command: "count-lines \(testFile.path)", expected: "20\n") try AssertExecuteCommand( - command: "count-lines \(testFile.path) --prefix al", expected: "4") + command: "count-lines \(testFile.path) --prefix al", expected: "4\n") } func testCountLinesHelp() throws { @@ -44,6 +44,8 @@ final class CountLinesExampleTests: XCTestCase { --prefix Only count lines with this prefix. --verbose Include extra information in the output. -h, --help Show help information. + + """ try AssertExecuteCommand(command: "count-lines -h", expected: helpText) } diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift index 81ec4907b..aa1fa7df7 100644 --- a/Tests/ArgumentParserExampleTests/MathExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -27,9 +27,9 @@ final class MathExampleTests: XCTestCase { } func testMath_Simple() throws { - try AssertExecuteCommand(command: "math 1 2 3 4 5", expected: "15") + try AssertExecuteCommand(command: "math 1 2 3 4 5", expected: "15\n") try AssertExecuteCommand( - command: "math multiply 1 2 3 4 5", expected: "120") + command: "math multiply 1 2 3 4 5", expected: "120\n") } func testMath_Help() throws { @@ -48,6 +48,7 @@ final class MathExampleTests: XCTestCase { stats Calculate descriptive statistics. See 'math help ' for detailed help. + """ try AssertExecuteCommand(command: "math -h", expected: helpText) @@ -68,6 +69,8 @@ final class MathExampleTests: XCTestCase { -x, --hex-output Use hexadecimal notation for the result. --version Show the version. -h, --help Show help information. + + """ try AssertExecuteCommand(command: "math add -h", expected: helpText) @@ -95,6 +98,8 @@ final class MathExampleTests: XCTestCase { median, mode; default: mean) --version Show the version. -h, --help Show help information. + + """ try AssertExecuteCommand( @@ -123,6 +128,8 @@ final class MathExampleTests: XCTestCase { --custom --version Show the version. -h, --help Show help information. + + """ // The "quantiles" subcommand's run() method is unimplemented, so it @@ -145,6 +152,7 @@ final class MathExampleTests: XCTestCase { Error: Please provide at least one value to calculate the mode. Usage: math stats average [--kind ] [ ...] See 'math stats average --help' for more information. + """, exitCode: .validationFailure) } @@ -152,13 +160,13 @@ final class MathExampleTests: XCTestCase { func testMath_Versions() throws { try AssertExecuteCommand( command: "math --version", - expected: "1.0.0") + expected: "1.0.0\n") try AssertExecuteCommand( command: "math stats --version", - expected: "1.0.0") + expected: "1.0.0\n") try AssertExecuteCommand( command: "math stats average --version", - expected: "1.5.0-alpha") + expected: "1.5.0-alpha\n") } func testMath_ExitCodes() throws { @@ -187,6 +195,7 @@ final class MathExampleTests: XCTestCase { Error: Unknown option '--foo' Usage: math add [--hex-output] [ ...] See 'math add --help' for more information. + """, exitCode: .validationFailure) @@ -197,6 +206,7 @@ final class MathExampleTests: XCTestCase { Help: A group of integers to operate on. Usage: math add [--hex-output] [ ...] See 'math add --help' for more information. + """, exitCode: .validationFailure) } @@ -246,7 +256,7 @@ extension MathExampleTests { "hello", "helicopter", "heliotrope", - ]), + ]) + "\n", environment: [ CompletionShell.shellEnvironmentVariableName: shell.rawValue ] @@ -258,7 +268,7 @@ extension MathExampleTests { "hello", "helicopter", "heliotrope", - ]), + ]) + "\n", environment: [ CompletionShell.shellEnvironmentVariableName: shell.rawValue ] @@ -269,7 +279,7 @@ extension MathExampleTests { expected: shell.format(completions: [ "aardvark", "aaaaalbert", - ]), + ]) + "\n", environment: [ CompletionShell.shellEnvironmentVariableName: shell.rawValue ] diff --git a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift index dcf0a74ca..ea6185129 100644 --- a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift @@ -25,6 +25,7 @@ final class RepeatExampleTests: XCTestCase { expected: """ hello hello + """) } @@ -34,6 +35,7 @@ final class RepeatExampleTests: XCTestCase { expected: """ 1: hello 2: hello + """) } @@ -47,6 +49,7 @@ final class RepeatExampleTests: XCTestCase { hello hello hello + """) } @@ -61,6 +64,8 @@ final class RepeatExampleTests: XCTestCase { --count The number of times to repeat 'phrase'. --include-counter Include a counter with each repetition. -h, --help Show help information. + + """ try AssertExecuteCommand(command: "repeat -h", expected: helpText) @@ -82,6 +87,8 @@ final class RepeatExampleTests: XCTestCase { --count The number of times to repeat 'phrase'. --include-counter Include a counter with each repetition. -h, --help Show help information. + + """, exitCode: .validationFailure) @@ -92,6 +99,7 @@ final class RepeatExampleTests: XCTestCase { Help: --count The number of times to repeat 'phrase'. Usage: repeat [--count ] [--include-counter] See 'repeat --help' for more information. + """, exitCode: .validationFailure) @@ -102,6 +110,7 @@ final class RepeatExampleTests: XCTestCase { Help: --count The number of times to repeat 'phrase'. Usage: repeat [--count ] [--include-counter] See 'repeat --help' for more information. + """, exitCode: .validationFailure) @@ -111,6 +120,7 @@ final class RepeatExampleTests: XCTestCase { Error: Unknown option '--version' Usage: repeat [--count ] [--include-counter] See 'repeat --help' for more information. + """, exitCode: .validationFailure) } diff --git a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift index dcfc72c4e..9567cc244 100644 --- a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift @@ -34,6 +34,8 @@ final class RollDiceExampleTests: XCTestCase { --seed A seed to use for repeatable random generation. -v, --verbose Show all roll results. -h, --help Show help information. + + """ try AssertExecuteCommand(command: "roll -h", expected: helpText) @@ -48,6 +50,7 @@ final class RollDiceExampleTests: XCTestCase { Help: --times Rolls the dice times. Usage: roll [--times ] [--sides ] [--seed ] [--verbose] See 'roll --help' for more information. + """, exitCode: .validationFailure) @@ -58,6 +61,7 @@ final class RollDiceExampleTests: XCTestCase { Help: --times Rolls the dice times. Usage: roll [--times ] [--sides ] [--seed ] [--verbose] See 'roll --help' for more information. + """, exitCode: .validationFailure) } diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testColorDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testColorDoccReference().md index f0c92a1f3..966fd9cdd 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testColorDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testColorDoccReference().md @@ -32,3 +32,8 @@ color help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testCountLinesDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testCountLinesDoccReference().md index d92527216..88b97b538 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testCountLinesDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testCountLinesDoccReference().md @@ -35,3 +35,8 @@ count-lines help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md index 762138958..adcaac132 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md @@ -205,3 +205,8 @@ math help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRepeatDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRepeatDoccReference().md index 5becb0f5c..8d0714b48 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRepeatDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRepeatDoccReference().md @@ -35,3 +35,8 @@ repeat help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRollDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRollDoccReference().md index 15f2b0bb7..d84e3e2b0 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRollDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRollDoccReference().md @@ -42,3 +42,8 @@ roll help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json index ca23cdfe2..7e180a263 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json @@ -213,4 +213,4 @@ ] }, "serializationVersion" : 0 -} +} \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json index ceb7b826b..1fe07a4e9 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json @@ -108,4 +108,4 @@ ] }, "serializationVersion" : 0 -} +} \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 64b7a47e8..599a68f4f 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -105,4 +105,4 @@ _base_test_help() { } -complete -F _base_test base-test +complete -F _base_test base-test \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish index 86510cbab..5636f921a 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish @@ -58,4 +58,4 @@ complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-comman complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -s h -l help -d 'Show help information.' complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -f -a 'sub-command' -d '' complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -f -a 'escaped-command' -d '' -complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -f -a 'help' -d 'Show subcommand help information.' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -f -a 'help' -d 'Show subcommand help information.' \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 1894128ef..ef2da963f 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -107,4 +107,4 @@ _base-test_help() { return "${ret}" } -_base-test +_base-test \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json index 5fa599a65..2e115c010 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json @@ -239,4 +239,4 @@ ] }, "serializationVersion" : 0 -} +} \ No newline at end of file From 6d32ef32e496b35cd1b1cf650f864aa2d7f9aa88 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Fri, 14 Feb 2025 21:57:40 -0500 Subject: [PATCH 33/33] Throw error if attempting to generate a zsh completion script for no commands. Force unwrap first in ZshCompletionsGenerator.swift. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../Completions/ZshCompletionsGenerator.swift | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 9232fd30c..9807a2bea 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -12,26 +12,31 @@ extension [ParsableCommand.Type] { /// Generates a Zsh completion script for the given command. var zshCompletionScript: String { - """ - #compdef \(first?._commandName ?? "") + // swift-format-ignore: NeverForceUnwrap + // Preconditions: + // - first must be non-empty for a zsh 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 """ + #compdef \(commandName) - \(completeFunctionName)() { - local -ar non_empty_completions=("${@:#(|:*)}") - local -ar empty_completions=("${(M)@:#(|:*)}") - _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' - } + \(completeFunctionName)() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' + } - \(customCompleteFunctionName)() { - local -a completions - completions=("${(@f)"$("${@}")"}") - if [[ "${#completions[@]}" -gt 1 ]]; then - \(completeFunctionName) "${completions[@]:0:-1}" - fi - } + \(customCompleteFunctionName)() { + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + \(completeFunctionName) "${completions[@]:0:-1}" + fi + } - \(completionFunctions)\ - \(completionFunctionName()) - """ + \(completionFunctions)\ + \(completionFunctionName()) + """ } private var completionFunctions: String { @@ -203,11 +208,15 @@ extension [ParsableCommand.Type] { } private var completeFunctionName: String { - "__\(first?._commandName ?? "")_complete" + // swift-format-ignore: NeverForceUnwrap + // Precondition: first is guaranteed to be non-empty + "__\(first!._commandName)_complete" } private var customCompleteFunctionName: String { - "__\(first?._commandName ?? "")_custom_complete" + // swift-format-ignore: NeverForceUnwrap + // Precondition: first is guaranteed to be non-empty + "__\(first!._commandName)_custom_complete" } }