Skip to content

Fix zsh/bash completions for arguments in option groups with a custom completion #648

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,8 @@ struct BashCompletionsGenerator {
case .shellCommand(let command):
return "$(\(command))"
case .custom:
// Generate a call back into the command to retrieve a completions list
let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ")
// TODO: Make this work for @Arguments
let argumentName = arg.names.preferredName?.synopsisString
?? arg.help.keys.first?.name ?? "---"

return """
$("${COMP_WORDS[0]}" ---completion \(subcommandNames) -- \(argumentName) "${COMP_WORDS[@]}")
$("${COMP_WORDS[0]}" \(arg.customCompletionCall(commands)) "${COMP_WORDS[@]}")
"""
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ extension ArgumentDefinition {
func customCompletionCall(_ commands: [ParsableCommand.Type]) -> String {
let subcommandNames = commands.dropFirst().map { $0._commandName }.joined(separator: " ")
let argumentName = names.preferredName?.synopsisString
?? self.help.keys.first?.name ?? "---"
?? self.help.keys.first?.fullPathString ?? "---"
return "---completion \(subcommandNames) -- \(argumentName)"
}
}
Expand Down
53 changes: 28 additions & 25 deletions Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct FishCompletionsGenerator {
preprocessorFunction(commandName: programName),
helperFunction(commandName: programName)
]
let completions = generateCompletions(commandChain: [programName], [type])
let completions = generateCompletions([type])

return helperFunctions.joined(separator: "\n\n") + "\n\n" + completions.joined(separator: "\n")
}
Expand All @@ -14,10 +14,10 @@ struct FishCompletionsGenerator {
// MARK: - Private functions

extension FishCompletionsGenerator {
private static func generateCompletions(commandChain: [String], _ commands: [ParsableCommand.Type]) -> [String] {
private static func generateCompletions(_ commands: [ParsableCommand.Type]) -> [String] {
let type = commands.last!
let isRootCommand = commands.count == 1
let programName = commandChain[0]
let programName = commands[0]._commandName
var subcommands = type.configuration.subcommands
.filter { $0.configuration.shouldDisplay }

Expand All @@ -27,7 +27,7 @@ extension FishCompletionsGenerator {

let helperFunctionName = helperFunctionName(commandName: programName)

var prefix = "complete -c \(programName) -n '\(helperFunctionName) \"\(commandChain.joined(separator: separator))\""
var prefix = "complete -c \(programName) -n '\(helperFunctionName) \"\(commands.map { $0._commandName }.joined(separator: separator))\""
if !subcommands.isEmpty {
prefix += " \"\(subcommands.map { $0._commandName }.joined(separator: separator))\""
}
Expand All @@ -45,50 +45,53 @@ extension FishCompletionsGenerator {

let argumentCompletions = commands
.argumentsForHelp(visibility: .default)
.compactMap { $0.argumentSegments(commandChain) }
.compactMap { $0.argumentSegments(commands) }
.map { $0.joined(separator: " ") }
.map { complete(suggestion: $0) }

let completionsFromSubcommands = subcommands.flatMap { subcommand in
generateCompletions(commandChain: commandChain + [subcommand._commandName], [subcommand])
generateCompletions(commands + [subcommand])
}

return completionsFromSubcommands + argumentCompletions + subcommandCompletions
}
}

extension ArgumentDefinition {
fileprivate func argumentSegments(_ commandChain: [String]) -> [String]? {
guard help.visibility.base == .default,
!names.isEmpty
fileprivate func argumentSegments(_ commands: [ParsableCommand.Type]) -> [String]? {
guard help.visibility.base == .default
else { return nil }

var results = names.map{ $0.asFishSuggestion }


var results: [String] = []

if !names.isEmpty {
results += names.map{ $0.asFishSuggestion }
}

if !help.abstract.isEmpty {
results += ["-d '\(help.abstract.fishEscape())'"]
}

if isNullary {
return results
}


switch completion.kind {
case .default: return results
case .default where names.isEmpty:
return nil
case .default:
break
case .list(let list):
return results + ["-r -f -k -a '\(list.joined(separator: " "))'"]
results += ["-r -f -k -a '\(list.joined(separator: " "))'"]
case .file(let extensions):
let pattern = "*.{\(extensions.joined(separator: ","))}"
return results + ["-r -f -a '(for i in \(pattern); echo $i;end)'"]
results += ["-r -f -a '(for i in \(pattern); echo $i;end)'"]
case .directory:
return results + ["-r -f -a '(__fish_complete_directories)'"]
results += ["-r -f -a '(__fish_complete_directories)'"]
case .shellCommand(let shellCommand):
return results + ["-r -f -a '(\(shellCommand))'"]
results += ["-r -f -a '(\(shellCommand))'"]
case .custom:
let program = commandChain[0]
let subcommands = commandChain.dropFirst().joined(separator: " ")
return results + ["-r -f -a '(command \(program) ---completion \(subcommands) -- --custom (commandline -opc)[1..-1])'"]
let commandName = commands.first!._commandName
results += ["-r -f -a '(command \(commandName) \(customCompletionCall(commands)) (commandline -opc)[1..-1])'"]
}

return results
}
}

Expand Down
5 changes: 2 additions & 3 deletions Sources/ArgumentParser/Parsing/ArgumentSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,9 @@ extension ArgumentSet {
}

func firstPositional(
named name: String
withKey key: InputKey
) -> ArgumentDefinition? {
let key = InputKey(name: name, parent: nil)
return first(where: { $0.help.keys.contains(key) })
first(where: { $0.help.keys.contains(key) })
}
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/ArgumentParser/Parsing/CommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ extension CommandParser {
completionFunction = f

case .value(let str):
guard let matchedArgument = argset.firstPositional(named: str),
guard let key = InputKey(fullPathString: str),
let matchedArgument = argset.firstPositional(withKey: key),
case .custom(let f) = matchedArgument.completion.kind
else { throw ParserError.invalidState }
completionFunction = f
Expand Down
19 changes: 18 additions & 1 deletion Sources/ArgumentParser/Parsing/InputKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ struct InputKey: Hashable {

extension InputKey: CustomStringConvertible {
var description: String {
fullPath.joined(separator: ".")
fullPathString
}
}

extension InputKey {
private static var separator: Character { "." }

var fullPathString: String {
fullPath.joined(separator: .init(Self.separator))
}

init?(fullPathString: String) {
let fullPath = fullPathString.split(separator: Self.separator).map(String.init)

guard let name = fullPath.last else { return nil }

self.name = name
self.path = fullPath.dropLast()
}
}
17 changes: 12 additions & 5 deletions Tests/ArgumentParserExampleTests/MathExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -608,23 +608,30 @@ function _swift_math_using_command
end

complete -c math -n \'_swift_math_using_command \"math add\"\' -l hex-output -s x -d \'Use hexadecimal notation for the result.\'
complete -c math -n \'_swift_math_using_command \"math add\"\' -l version -d \'Show the version.\'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The completions now include the --version flag like the bash/zsh commands.

complete -c math -n \'_swift_math_using_command \"math add\"\' -s h -l help -d \'Show help information.\'
complete -c math -n \'_swift_math_using_command \"math multiply\"\' -l hex-output -s x -d \'Use hexadecimal notation for the result.\'
complete -c math -n \'_swift_math_using_command \"math multiply\"\' -l version -d \'Show the version.\'
complete -c math -n \'_swift_math_using_command \"math multiply\"\' -s h -l help -d \'Show help information.\'
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -l kind -d \'The kind of average to provide.\' -r -f -k -a \'mean median mode\'
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -l version -d \'Show the version.\'
complete -c math -n \'_swift_math_using_command \"math stats average\"\' -s h -l help -d \'Show help information.\'
complete -c math -n \'_swift_math_using_command \"math stats stdev\"\' -l version -d \'Show the version.\'
complete -c math -n \'_swift_math_using_command \"math stats stdev\"\' -s h -l help -d \'Show help information.\'
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -r -f -k -a \'alphabet alligator branch braggart\'
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -r -f -a \'(command math ---completion stats quantiles -- customArg (commandline -opc)[1..-1])\'
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l file -r -f -a \'(for i in *.{txt,md}; echo $i;end)\'
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l directory -r -f -a \'(__fish_complete_directories)\'
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l shell -r -f -a \'(head -100 /usr/share/dict/words | tail -50)\'
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l custom -r -f -a \'(command math ---completion stats quantiles -- --custom (commandline -opc)[1..-1])\'
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -l version -d \'Show the version.\'
complete -c math -n \'_swift_math_using_command \"math stats quantiles\"\' -s h -l help -d \'Show help information.\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -s h -l help -d \'Show help information.\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'average\' -d \'Print the average of the values.\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'stdev\' -d \'Print the standard deviation of the values.\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'quantiles\' -d \'Print the quantiles of the values (TBD).\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles help\"\' -f -a \'help\' -d \'Show subcommand help information.\'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes seems to have also fixed another bug where it could suggest help following subcommands when it isn't a valid subcommand.

complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -l version -d \'Show the version.\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -s h -l help -d \'Show help information.\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -f -a \'average\' -d \'Print the average of the values.\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -f -a \'stdev\' -d \'Print the standard deviation of the values.\'
complete -c math -n \'_swift_math_using_command \"math stats\" \"average stdev quantiles\"\' -f -a \'quantiles\' -d \'Print the quantiles of the values (TBD).\'
complete -c math -n \'_swift_math_using_command \"math help\"\' -l version -d \'Show the version.\'
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -l version -d \'Show the version.\'
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -s h -l help -d \'Show help information.\'
complete -c math -n \'_swift_math_using_command \"math\" \"add multiply stats help\"\' -f -a \'add\' -d \'Print the sum of the values.\'
Expand Down
35 changes: 29 additions & 6 deletions Tests/ArgumentParserUnitTests/CompletionScriptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ extension CompletionScriptTests {
case one, two, three = "custom-three"
}

struct NestedArguments: ParsableArguments {
@Argument(completion: .custom { _ in ["t", "u", "v"] })
var nestedArgument: String
}

struct Base: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "base-test",
Expand All @@ -53,12 +58,15 @@ extension CompletionScriptTests {

@Option() var rep1: [String]
@Option(name: [.short, .long]) var rep2: [String]

struct SubCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "sub-command"
)
}

@Argument(completion: .custom { _ in ["q", "r", "s"] }) var argument: String
@OptionGroup var nested: NestedArguments

struct SubCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "sub-command"
)
}
}

func testBase_Zsh() throws {
Expand Down Expand Up @@ -111,6 +119,13 @@ extension CompletionScriptTests {

@Option(name: .customShort("z"), completion: .custom { _ in ["x", "y", "z"] })
var three: String

@OptionGroup var nested: NestedArguments

struct NestedArguments: ParsableArguments {
@Argument(completion: .custom { _ in ["g", "h", "i"] })
var four: String
}
}

func verifyCustomOutput(
Expand All @@ -134,8 +149,10 @@ extension CompletionScriptTests {
try verifyCustomOutput("--one", expectedOutput: "a\nb\nc")
try verifyCustomOutput("two", expectedOutput: "d\ne\nf")
try verifyCustomOutput("-z", expectedOutput: "x\ny\nz")
try verifyCustomOutput("nested.four", expectedOutput: "g\nh\ni")

XCTAssertThrowsError(try verifyCustomOutput("--bad", expectedOutput: ""))
XCTAssertThrowsError(try verifyCustomOutput("four", expectedOutput: ""))
}
}

Expand Down Expand Up @@ -175,6 +192,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}'
'(-h --help)'{-h,--help}'[Show help information.]'
'(-): :->command'
'(-)*:: :->arg'
Expand Down Expand Up @@ -243,6 +262,8 @@ _base_test() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
COMPREPLY=()
opts="--name --kind --other-kind --path1 --path2 --path3 --one --two --three --kind-counter --rep1 -r --rep2 -h --help sub-command 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
Expand Down Expand Up @@ -401,6 +422,8 @@ 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 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.'
Expand Down