Skip to content

Commit

Permalink
Add singleton to indicate which shell is requesting completion candid…
Browse files Browse the repository at this point in the history
…ates

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 apple#672

Signed-off-by: Ross Goldberg <[email protected]>
  • Loading branch information
rgoldberg committed Nov 1, 2024
1 parent 72bf212 commit 9577ddc
Show file tree
Hide file tree
Showing 9 changed files with 702 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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=()
Expand Down
12 changes: 12 additions & 0 deletions Sources/ArgumentParser/Completions/CompletionsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
11 changes: 7 additions & 4 deletions Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down Expand Up @@ -83,7 +87,7 @@ struct ZshCompletionsGenerator {
}

let functionText = """
\(functionName)() {
\(functionName)() {\(exportSAPShellForRootCommand)
integer ret=1
local -a args
args+=(
Expand Down Expand Up @@ -208,4 +212,3 @@ extension ArgumentDefinition {
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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 []
}
}
```
11 changes: 11 additions & 0 deletions Sources/ArgumentParser/Parsing/CommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
3 changes: 3 additions & 0 deletions Tests/ArgumentParserExampleTests/MathExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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=()
Expand Down Expand Up @@ -395,6 +396,7 @@ _math_commandname=$words[1]
typeset -A opt_args
_math() {
export SAP_SHELL=zsh
integer ret=1
local -a args
args+=(
Expand Down Expand Up @@ -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])
Expand Down
7 changes: 7 additions & 0 deletions Tests/ArgumentParserUnitTests/CompletionScriptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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+=(
Expand Down Expand Up @@ -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=()
Expand Down Expand Up @@ -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+=(
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -486,6 +490,7 @@ _parent_commandname=$words[1]
typeset -A opt_args
_parent() {
export SAP_SHELL=zsh
integer ret=1
local -a args
args+=(
Expand All @@ -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=()
Expand Down Expand Up @@ -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])
Expand Down
Loading

0 comments on commit 9577ddc

Please sign in to comment.