From ed8001c7ec93d5f044a97674c354e5deafab0f09 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Thu, 31 Oct 2024 04:16:26 -0400 Subject: [PATCH] Singleton to indicate which shell is requesting completion candidates A CompletionShell singleton named CompletionShell.requesting has been created that indicates which shell is requesting completion candidates. The singleton is populated when a completion script is generated, so functions used to generate arguments for CompletionKind creation functions can return completion candidate syntax / shell commands tailored for that shell. For the custom(:) CompletionKind creation function, the singleton is populated at runtime (when a completion script requests completions from the Swift app after a user types tab while composing a command line to call the app). The requesting shell is communicated to the Swift app via an environment variable named SAP_SHELL, which is exported by each of the generated completion scripts. Resolve #672 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .../BashCompletionsGenerator.swift | 1 + .../Completions/CompletionsGenerator.swift | 12 + .../FishCompletionsGenerator.swift | 1 + .../Completions/ZshCompletionsGenerator.swift | 11 +- .../Articles/CustomizingCompletions.md | 49 ++ .../Parsing/CommandParser.swift | 11 + .../MathExampleTests.swift | 3 + .../CompletionScriptTests.swift | 7 + .../RequestingCompletionScriptTests.swift | 611 ++++++++++++++++++ 9 files changed, 702 insertions(+), 4 deletions(-) create mode 100644 Tests/ArgumentParserUnitTests/RequestingCompletionScriptTests.swift diff --git a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift index cd7864b5c..8d5d8aed1 100644 --- a/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift @@ -57,6 +57,7 @@ struct BashCompletionsGenerator { // that other command functions don't need. if isRootCommand { result += """ + export \(CompletionShell.environmentVariableName)=bash cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index bbfe0bb72..99471ec5e 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -41,6 +41,17 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { public static var allCases: [CompletionShell] { [.zsh, .bash, .fish] } + + /// An instance representing the shell for which completions are being + /// requested. + public static var requesting: CompletionShell? + + /// The name of the environment variable whose value is the name of the shell + /// for which completions are being requested from a custom completion + /// handler. + /// + /// The environment variable is set in generated completion scripts. + static let environmentVariableName = "SAP_SHELL" } struct CompletionsGenerator { @@ -69,6 +80,7 @@ struct CompletionsGenerator { /// Generates a Bash completion script for this generators shell and command.. func generateCompletionScript() -> String { + CompletionShell.requesting = shell switch shell { case .zsh: return ZshCompletionsGenerator.generateCompletionScript(command) diff --git a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift index 454ca2a9a..6774d34c2 100644 --- a/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift @@ -147,6 +147,7 @@ extension FishCompletionsGenerator { let preprocessorFunctionName = preprocessorFunctionName(commandName: commandName) return """ function \(functionName) + set -x \(CompletionShell.environmentVariableName) fish set -l currentCommands (\(preprocessorFunctionName) (commandline -opc)) set -l expectedCommands (string split \"\(separator)\" $argv[1]) set -l subcommands (string split \"\(separator)\" $argv[2]) diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 96143bd02..3d928691e 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -34,8 +34,12 @@ struct ZshCompletionsGenerator { let type = commands.last! let functionName = commands.completionFunctionName() let isRootCommand = commands.count == 1 - - var args = generateCompletionArguments(commands) + + let exportSAPShellForRootCommand = isRootCommand + ? "\n export \(CompletionShell.environmentVariableName)=zsh" + : "" + + var args = generateCompletionArguments(commands) var subcommands = type.configuration.subcommands .filter { $0.configuration.shouldDisplay } var subcommandHandler = "" @@ -83,7 +87,7 @@ struct ZshCompletionsGenerator { } let functionText = """ - \(functionName)() { + \(functionName)() {\(exportSAPShellForRootCommand) integer ret=1 local -a args args+=( @@ -208,4 +212,3 @@ extension ArgumentDefinition { } } } - diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCompletions.md b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCompletions.md index 0876e0141..f08509d08 100644 --- a/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCompletions.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCompletions.md @@ -60,3 +60,52 @@ struct SwiftRun { ``` In this example, when a user requests completions for the `--target` option, the completion script runs the `SwiftRun` command-line tool with a special syntax, calling the `listExecutables` function with an array of the arguments given so far. + +### Configuring CompletionKind per Shell + +The shells supported for parameter completion all have different completion candidate formats, +as well as their own different syntaxes and built-in commands. + +The `CompletionShell.requesting` singleton (of type `CompletionShell?`) can be read to determine +which shell is requesting completion candidates when evaluating functions that either provide +arguments to a `CompletionKind` creation function, or that are themselves arguments to a +`CompletionKind` creation function. e.g.: + +```swift +struct Tool { + @Option(completion: .shellCommand(generateCommandPerShell())) + var x: String? + + @Option(completion: .custom(generateCompletionCandidatesPerShell)) + var y: String? +} + +/// Runs when a completion script is generated; results hardcoded into script. +func generateCommandPerShell() -> String { + switch CompletionShell.requesting { + case CompletionShell.bash: + return "bash-specific script" + case CompletionShell.fish: + return "fish-specific script" + case CompletionShell.zsh: + return "zsh-specific script" + default: + // return a universal no-op for unknown shells + return ":" + } +} + +/// Runs during completion while user is typing command line to use your tool +func generateCompletionCandidatesPerShell(_ arguments: [String]) -> [String] { + switch CompletionShell.requesting { + case CompletionShell.bash: + return ["A:in:bash:syntax", "B:in:bash:syntax", "C:in:bash:syntax"] + case CompletionShell.fish: + return ["A:in:fish:syntax", "B:in:bash:syntax", "C:in:bash:syntax"] + case CompletionShell.zsh: + return ["A:in:zsh:syntax", "B:in:zsh:syntax", "C:in:zsh:syntax"] + default: + return [] + } +} +``` diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index c09220b34..0beaad211 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -9,6 +9,14 @@ // //===----------------------------------------------------------------------===// +#if swift(>=5.11) +internal import class Foundation.ProcessInfo +#elseif swift(>=5.10) +import class Foundation.ProcessInfo +#else +@_implementationOnly import class Foundation.ProcessInfo +#endif + struct CommandError: Error { var commandStack: [ParsableCommand.Type] var parserError: ParserError @@ -356,6 +364,9 @@ 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. + if let completionShellName = ProcessInfo.processInfo.environment[CompletionShell.environmentVariableName] { + CompletionShell.requesting = CompletionShell(rawValue: completionShellName) + } let output = completionFunction(completionValues).joined(separator: "\n") throw ParserError.completionScriptCustomResponse(output) } diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift index ee677a6a0..b2331f473 100644 --- a/Tests/ArgumentParserExampleTests/MathExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -241,6 +241,7 @@ private let bashCompletionScriptText = """ #!/bin/bash _math() { + export SAP_SHELL=bash cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() @@ -395,6 +396,7 @@ _math_commandname=$words[1] typeset -A opt_args _math() { + export SAP_SHELL=zsh integer ret=1 local -a args args+=( @@ -583,6 +585,7 @@ function _swift_math_preprocessor end function _swift_math_using_command + set -x SAP_SHELL fish set -l currentCommands (_swift_math_preprocessor (commandline -opc)) set -l expectedCommands (string split \" \" $argv[1]) set -l subcommands (string split \" \" $argv[2]) diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift index 62f793add..70a9b53cc 100644 --- a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift +++ b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift @@ -177,6 +177,7 @@ _base_test_commandname=$words[1] typeset -A opt_args _base-test() { + export SAP_SHELL=zsh integer ret=1 local -a args args+=( @@ -258,6 +259,7 @@ private let bashBaseCompletions = """ #!/bin/bash _base_test() { + export SAP_SHELL=bash cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() @@ -350,6 +352,7 @@ _escaped_command_commandname=$words[1] typeset -A opt_args _escaped-command() { + export SAP_SHELL=zsh integer ret=1 local -a args args+=( @@ -385,6 +388,7 @@ function _swift_base-test_preprocessor end function _swift_base-test_using_command + set -x SAP_SHELL fish set -l currentCommands (_swift_base-test_preprocessor (commandline -opc)) set -l expectedCommands (string split " " $argv[1]) set -l subcommands (string split " " $argv[2]) @@ -486,6 +490,7 @@ _parent_commandname=$words[1] typeset -A opt_args _parent() { + export SAP_SHELL=zsh integer ret=1 local -a args args+=( @@ -509,6 +514,7 @@ let bashHiddenCompletion = """ #!/bin/bash _parent() { + export SAP_SHELL=bash cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() @@ -538,6 +544,7 @@ function _swift_parent_preprocessor end function _swift_parent_using_command + set -x SAP_SHELL fish set -l currentCommands (_swift_parent_preprocessor (commandline -opc)) set -l expectedCommands (string split " " $argv[1]) set -l subcommands (string split " " $argv[2]) diff --git a/Tests/ArgumentParserUnitTests/RequestingCompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/RequestingCompletionScriptTests.swift new file mode 100644 index 000000000..8d7f633ac --- /dev/null +++ b/Tests/ArgumentParserUnitTests/RequestingCompletionScriptTests.swift @@ -0,0 +1,611 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +@testable import ArgumentParser + +private func candidates(prefix: String) -> [String] { + switch CompletionShell.requesting { + case CompletionShell.bash: + return ["\(prefix)1_bash", "\(prefix)2_bash", "\(prefix)3_bash"] + case CompletionShell.fish: + return ["\(prefix)1_fish", "\(prefix)2_fish", "\(prefix)3_fish"] + case CompletionShell.zsh: + return ["\(prefix)1_zsh", "\(prefix)2_zsh", "\(prefix)3_zsh"] + default: + return [] + } +} + +final class RequestingCompletionScriptTests: XCTestCase { +} + +extension RequestingCompletionScriptTests { + struct Path: ExpressibleByArgument { + var path: String + + init?(argument: String) { + self.path = argument + } + + static var defaultCompletionKind: CompletionKind { + .file() + } + } + + enum Kind: String, ExpressibleByArgument, EnumerableFlag { + case one, two, three = "custom-three" + } + + struct NestedArguments: ParsableArguments { + @Argument(completion: .custom { _ in candidates(prefix: "a") }) + var nestedArgument: String + } + + struct Base: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "base-test", + subcommands: [SubCommand.self] + ) + + @Option(help: "The user's name.") var name: String + @Option() var kind: Kind + @Option(completion: .list(candidates(prefix: "b"))) var otherKind: Kind + + @Option() var path1: Path + @Option() var path2: Path? + @Option(completion: .list(candidates(prefix: "c"))) var path3: Path + + @Flag(help: .hidden) var verbose = false + @Flag var allowedKinds: [Kind] = [] + @Flag var kindCounter: Int + + @Option() var rep1: [String] + @Option(name: [.short, .long]) var rep2: [String] + + @Argument(completion: .custom { _ in candidates(prefix: "d") }) var argument: String + @OptionGroup var nested: NestedArguments + + struct SubCommand: ParsableCommand { + static let configuration = CommandConfiguration( + commandName: "sub-command" + ) + } + } + + func testBase_Zsh() throws { + let script1 = try CompletionsGenerator(command: Base.self, shell: .zsh) + .generateCompletionScript() + XCTAssertEqual(zshRequestingBaseCompletions, script1) + + let script2 = try CompletionsGenerator(command: Base.self, shellName: "zsh") + .generateCompletionScript() + XCTAssertEqual(zshRequestingBaseCompletions, script2) + + let script3 = Base.completionScript(for: .zsh) + XCTAssertEqual(zshRequestingBaseCompletions, script3) + } + + func testBase_Bash() throws { + let script1 = try CompletionsGenerator(command: Base.self, shell: .bash) + .generateCompletionScript() + XCTAssertEqual(bashRequestingBaseCompletions, script1) + + let script2 = try CompletionsGenerator(command: Base.self, shellName: "bash") + .generateCompletionScript() + XCTAssertEqual(bashRequestingBaseCompletions, script2) + + let script3 = Base.completionScript(for: .bash) + XCTAssertEqual(bashRequestingBaseCompletions, script3) + } + + func testBase_Fish() throws { + let script1 = try CompletionsGenerator(command: Base.self, shell: .fish) + .generateCompletionScript() + XCTAssertEqual(fishRequestingBaseCompletions, script1) + + let script2 = try CompletionsGenerator(command: Base.self, shellName: "fish") + .generateCompletionScript() + XCTAssertEqual(fishRequestingBaseCompletions, script2) + + let script3 = Base.completionScript(for: .fish) + XCTAssertEqual(fishRequestingBaseCompletions, script3) + } +} + +extension RequestingCompletionScriptTests { + struct Custom: ParsableCommand { + @Option(name: .shortAndLong, completion: .custom { _ in candidates(prefix: "e") }) + var one: String + + @Argument(completion: .custom { _ in candidates(prefix: "f") }) + var two: String + + @Option(name: .customShort("z"), completion: .custom { _ in candidates(prefix: "g") }) + var three: String + + @OptionGroup var nested: NestedArguments + + struct NestedArguments: ParsableArguments { + @Argument(completion: .custom { _ in candidates(prefix: "h") }) + var four: String + } + } + + func verifyCustomOutput( + _ arg: String, + forShell shell: String, + expectedOutputPrefix prefix: String, + file: StaticString = #filePath, + line: UInt = #line + ) throws { + do { + setenv("SAP_SHELL", shell, 1) + defer { + unsetenv("SAP_SHELL") + } + _ = try Custom.parse(["---completion", "--", arg]) + XCTFail("Didn't error as expected", file: (file), line: line) + } catch let error as CommandError { + guard case .completionScriptCustomResponse(let output) = error.parserError else { + throw error + } + XCTAssertEqual( + prefix.isEmpty + ? "" + : "\(prefix)1_\(shell)\n\(prefix)2_\(shell)\n\(prefix)3_\(shell)", + output, + file: (file), + line: line + ) + } + } + + func testCustomCompletions(forShell shell: String) throws { + try verifyCustomOutput("-o", forShell: shell, expectedOutputPrefix: "e") + try verifyCustomOutput("--one", forShell: shell, expectedOutputPrefix: "e") + try verifyCustomOutput("two", forShell: shell, expectedOutputPrefix: "f") + try verifyCustomOutput("-z", forShell: shell, expectedOutputPrefix: "g") + try verifyCustomOutput("nested.four", forShell: shell, expectedOutputPrefix: "h") + + XCTAssertThrowsError(try verifyCustomOutput("--bad", forShell: shell, expectedOutputPrefix: "")) + XCTAssertThrowsError(try verifyCustomOutput("four", forShell: shell, expectedOutputPrefix: "")) + } + + func testBashCustomCompletions() throws { + try testCustomCompletions(forShell: "bash") + } + + func testFishCustomCompletions() throws { + try testCustomCompletions(forShell: "fish") + } + + func testZshCustomCompletions() throws { + try testCustomCompletions(forShell: "zsh") + } +} + +extension RequestingCompletionScriptTests { + struct EscapedCommand: ParsableCommand { + @Option(help: #"Escaped chars: '[]\."#) + var one: String + + @Argument(completion: .custom { _ in candidates(prefix: "i") }) + var two: String + } + + func testEscaped_Zsh() throws { + XCTAssertEqual(zshRequestingEscapedCompletion, EscapedCommand.completionScript(for: .zsh)) + } +} + +let zshRequestingBaseCompletions = """ +#compdef base-test +local context state state_descr line +_base_test_commandname=$words[1] +typeset -A opt_args + +_base-test() { + export SAP_SHELL=zsh + integer ret=1 + local -a args + args+=( + '--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' + '--path2:path2:_files' + '--path3:path3:(c1_zsh c2_zsh c3_zsh)' + '--one' + '--two' + '--three' + '*--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}' + '(-h --help)'{-h,--help}'[Show help information.]' + '(-): :->command' + '(-)*:: :->arg' + ) + _arguments -w -s -S $args[@] && ret=0 + case $state in + (command) + local subcommands + subcommands=( + 'sub-command:' + 'help:Show subcommand help information.' + ) + _describe "subcommand" subcommands + ;; + (arg) + case ${words[1]} in + (sub-command) + _base-test_sub-command + ;; + (help) + _base-test_help + ;; + esac + ;; + esac + + return ret +} + +_base-test_sub-command() { + integer ret=1 + local -a args + args+=( + '(-h --help)'{-h,--help}'[Show help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + +_base-test_help() { + integer ret=1 + local -a args + args+=( + ':subcommands:' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + + +_custom_completion() { + local completions=("${(@f)$($*)}") + _describe '' completions +} + +_base-test +""" + +let bashRequestingBaseCompletions = """ +#!/bin/bash + +_base_test() { + export SAP_SHELL=bash + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + opts="--name --kind --other-kind --path1 --path2 --path3 --one --two --three --kind-counter --rep1 -r --rep2 -h --help sub-command help" + opts="$opts $("${COMP_WORDS[0]}" ---completion -- argument "${COMP_WORDS[@]}")" + opts="$opts $("${COMP_WORDS[0]}" ---completion -- nested.nestedArgument "${COMP_WORDS[@]}")" + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + case $prev in + --name) + + return + ;; + --kind) + COMPREPLY=( $(compgen -W "one two custom-three" -- "$cur") ) + return + ;; + --other-kind) + COMPREPLY=( $(compgen -W "b1_bash b2_bash b3_bash" -- "$cur") ) + return + ;; + --path1) + if declare -F _filedir >/dev/null; then + _filedir + else + COMPREPLY=( $(compgen -f -- "$cur") ) + fi + return + ;; + --path2) + if declare -F _filedir >/dev/null; then + _filedir + else + COMPREPLY=( $(compgen -f -- "$cur") ) + fi + return + ;; + --path3) + COMPREPLY=( $(compgen -W "c1_bash c2_bash c3_bash" -- "$cur") ) + return + ;; + --rep1) + + return + ;; + -r|--rep2) + + return + ;; + esac + case ${COMP_WORDS[1]} in + (sub-command) + _base_test_sub-command 2 + return + ;; + (help) + _base_test_help 2 + return + ;; + esac + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_base_test_sub_command() { + opts="-h --help" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} +_base_test_help() { + opts="" + if [[ $COMP_CWORD == "$1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + +complete -F _base_test base-test +""" + +let zshRequestingEscapedCompletion = """ +#compdef escaped-command +local context state state_descr line +_escaped_command_commandname=$words[1] +typeset -A opt_args + +_escaped-command() { + export SAP_SHELL=zsh + integer ret=1 + local -a args + args+=( + '--one[Escaped chars: '"'"'\\[\\]\\\\.]:one:' + ':two:{_custom_completion $_escaped_command_commandname ---completion -- two $words}' + '(-h --help)'{-h,--help}'[Show help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + + +_custom_completion() { + local completions=("${(@f)$($*)}") + _describe '' completions +} + +_escaped-command +""" + +let fishRequestingBaseCompletions = """ +# A function which filters options which starts with "-" from $argv. +function _swift_base-test_preprocessor + set -l results + for i in (seq (count $argv)) + switch (echo $argv[$i] | string sub -l 1) + case '-' + case '*' + echo $argv[$i] + end + end +end + +function _swift_base-test_using_command + set -x SAP_SHELL fish + set -l currentCommands (_swift_base-test_preprocessor (commandline -opc)) + set -l expectedCommands (string split " " $argv[1]) + set -l subcommands (string split " " $argv[2]) + if [ (count $currentCommands) -ge (count $expectedCommands) ] + for i in (seq (count $expectedCommands)) + if [ $currentCommands[$i] != $expectedCommands[$i] ] + return 1 + end + end + if [ (count $currentCommands) -eq (count $expectedCommands) ] + return 0 + end + if [ (count $subcommands) -gt 1 ] + for i in (seq (count $subcommands)) + if [ $currentCommands[(math (count $expectedCommands) + 1)] = $subcommands[$i] ] + return 1 + end + end + end + return 0 + end + return 1 +end + +complete -c base-test -n '_swift_base-test_using_command "base-test sub-command"' -s h -l help -d 'Show help information.' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l name -d 'The user\\'s name.' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l kind -r -f -k -a 'one two custom-three' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l other-kind -r -f -k -a 'b1_fish b2_fish b3_fish' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l path1 -r -f -a '(for i in *.{}; echo $i;end)' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l path2 -r -f -a '(for i in *.{}; echo $i;end)' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l path3 -r -f -k -a 'c1_fish c2_fish c3_fish' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l one +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l two +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l three +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l kind-counter +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l rep1 +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -s r -l rep2 +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -r -f -a '(command base-test ---completion -- argument (commandline -opc)[1..-1])' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -r -f -a '(command base-test ---completion -- nested.nestedArgument (commandline -opc)[1..-1])' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -s h -l help -d 'Show help information.' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -f -a 'sub-command' -d '' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -f -a 'help' -d 'Show subcommand help information.' +""" + +// MARK: - Test Hidden Subcommand +struct RequestingParent: ParsableCommand { + static let configuration = CommandConfiguration(subcommands: [RequestingHiddenChild.self]) +} + +struct RequestingHiddenChild: ParsableCommand { + static let configuration = CommandConfiguration(shouldDisplay: false) +} + +extension RequestingCompletionScriptTests { + func testHiddenSubcommand_Zsh() throws { + let script1 = try CompletionsGenerator(command: Parent.self, shell: .zsh) + .generateCompletionScript() + XCTAssertEqual(zshRequestingHiddenCompletion, script1) + + let script2 = try CompletionsGenerator(command: Parent.self, shellName: "zsh") + .generateCompletionScript() + XCTAssertEqual(zshRequestingHiddenCompletion, script2) + + let script3 = Parent.completionScript(for: .zsh) + XCTAssertEqual(zshRequestingHiddenCompletion, script3) + } + + func testHiddenSubcommand_Bash() throws { + let script1 = try CompletionsGenerator(command: Parent.self, shell: .bash) + .generateCompletionScript() + XCTAssertEqual(bashRequestingHiddenCompletion, script1) + + let script2 = try CompletionsGenerator(command: Parent.self, shellName: "bash") + .generateCompletionScript() + XCTAssertEqual(bashRequestingHiddenCompletion, script2) + + let script3 = Parent.completionScript(for: .bash) + XCTAssertEqual(bashRequestingHiddenCompletion, script3) + } + + func testHiddenSubcommand_Fish() throws { + let script1 = try CompletionsGenerator(command: Parent.self, shell: .fish) + .generateCompletionScript() + XCTAssertEqual(fishRequestingHiddenCompletion, script1) + + let script2 = try CompletionsGenerator(command: Parent.self, shellName: "fish") + .generateCompletionScript() + XCTAssertEqual(fishRequestingHiddenCompletion, script2) + + let script3 = Parent.completionScript(for: .fish) + XCTAssertEqual(fishRequestingHiddenCompletion, script3) + } +} + +let zshRequestingHiddenCompletion = """ +#compdef parent +local context state state_descr line +_parent_commandname=$words[1] +typeset -A opt_args + +_parent() { + export SAP_SHELL=zsh + integer ret=1 + local -a args + args+=( + '(-h --help)'{-h,--help}'[Show help information.]' + ) + _arguments -w -s -S $args[@] && ret=0 + + return ret +} + + +_custom_completion() { + local completions=("${(@f)$($*)}") + _describe '' completions +} + +_parent +""" + +let bashRequestingHiddenCompletion = """ +#!/bin/bash + +_parent() { + export SAP_SHELL=bash + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + COMPREPLY=() + opts="-h --help" + if [[ $COMP_CWORD == "1" ]]; then + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) + return + fi + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + + +complete -F _parent parent +""" + +let fishRequestingHiddenCompletion = """ +# A function which filters options which starts with "-" from $argv. +function _swift_parent_preprocessor + set -l results + for i in (seq (count $argv)) + switch (echo $argv[$i] | string sub -l 1) + case '-' + case '*' + echo $argv[$i] + end + end +end + +function _swift_parent_using_command + set -x SAP_SHELL fish + set -l currentCommands (_swift_parent_preprocessor (commandline -opc)) + set -l expectedCommands (string split " " $argv[1]) + set -l subcommands (string split " " $argv[2]) + if [ (count $currentCommands) -ge (count $expectedCommands) ] + for i in (seq (count $expectedCommands)) + if [ $currentCommands[$i] != $expectedCommands[$i] ] + return 1 + end + end + if [ (count $currentCommands) -eq (count $expectedCommands) ] + return 0 + end + if [ (count $subcommands) -gt 1 ] + for i in (seq (count $subcommands)) + if [ $currentCommands[(math (count $expectedCommands) + 1)] = $subcommands[$i] ] + return 1 + end + end + end + return 0 + end + return 1 +end + +complete -c parent -n '_swift_parent_using_command \"parent\"' -s h -l help -d 'Show help information.' +"""