Skip to content

Commit 30e0f78

Browse files
committed
Add 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 <[email protected]>
1 parent d6836f4 commit 30e0f78

File tree

9 files changed

+702
-4
lines changed

9 files changed

+702
-4
lines changed

Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ struct BashCompletionsGenerator {
5757
// that other command functions don't need.
5858
if isRootCommand {
5959
result += """
60+
export \(CompletionShell.environmentVariableName)=bash
6061
cur="${COMP_WORDS[COMP_CWORD]}"
6162
prev="${COMP_WORDS[COMP_CWORD-1]}"
6263
COMPREPLY=()

Sources/ArgumentParser/Completions/CompletionsGenerator.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable {
4141
public static var allCases: [CompletionShell] {
4242
[.zsh, .bash, .fish]
4343
}
44+
45+
/// An instance representing the shell for which completions are being
46+
/// requested.
47+
public internal(set) static var requesting: CompletionShell?
48+
49+
/// The name of the environment variable whose value is the name of the shell
50+
/// for which completions are being requested from a custom completion
51+
/// handler.
52+
///
53+
/// The environment variable is set in generated completion scripts.
54+
static let environmentVariableName = "SAP_SHELL"
4455
}
4556

4657
struct CompletionsGenerator {
@@ -69,6 +80,7 @@ struct CompletionsGenerator {
6980

7081
/// Generates a Bash completion script for this generators shell and command..
7182
func generateCompletionScript() -> String {
83+
CompletionShell.requesting = shell
7284
switch shell {
7385
case .zsh:
7486
return ZshCompletionsGenerator.generateCompletionScript(command)

Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ extension FishCompletionsGenerator {
147147
let preprocessorFunctionName = preprocessorFunctionName(commandName: commandName)
148148
return """
149149
function \(functionName)
150+
set -x \(CompletionShell.environmentVariableName) fish
150151
set -l currentCommands (\(preprocessorFunctionName) (commandline -opc))
151152
set -l expectedCommands (string split \"\(separator)\" $argv[1])
152153
set -l subcommands (string split \"\(separator)\" $argv[2])

Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,12 @@ struct ZshCompletionsGenerator {
3434
let type = commands.last!
3535
let functionName = commands.completionFunctionName()
3636
let isRootCommand = commands.count == 1
37-
38-
var args = generateCompletionArguments(commands)
37+
38+
let exportSAPShellForRootCommand = isRootCommand
39+
? "\n export \(CompletionShell.environmentVariableName)=zsh"
40+
: ""
41+
42+
var args = generateCompletionArguments(commands)
3943
var subcommands = type.configuration.subcommands
4044
.filter { $0.configuration.shouldDisplay }
4145
var subcommandHandler = ""
@@ -83,7 +87,7 @@ struct ZshCompletionsGenerator {
8387
}
8488

8589
let functionText = """
86-
\(functionName)() {
90+
\(functionName)() {\(exportSAPShellForRootCommand)
8791
integer ret=1
8892
local -a args
8993
args+=(
@@ -208,4 +212,3 @@ extension ArgumentDefinition {
208212
}
209213
}
210214
}
211-

Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCompletions.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,52 @@ struct SwiftRun {
6060
```
6161

6262
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.
63+
64+
### Configuring Completion Candidates per Shell
65+
66+
The shells supported for parameter completion all have different completion candidate formats,
67+
as well as their own different syntaxes and built-in commands.
68+
69+
The `CompletionShell.requesting` singleton (of type `CompletionShell?`) can be read to determine
70+
which shell is requesting completion candidates when evaluating functions that either provide
71+
arguments to a `CompletionKind` creation function, or that are themselves arguments to a
72+
`CompletionKind` creation function. e.g.:
73+
74+
```swift
75+
struct Tool {
76+
@Option(completion: .shellCommand(generateCommandPerShell()))
77+
var x: String?
78+
79+
@Option(completion: .custom(generateCompletionCandidatesPerShell))
80+
var y: String?
81+
}
82+
83+
/// Runs when a completion script is generated; results hardcoded into script.
84+
func generateCommandPerShell() -> String {
85+
switch CompletionShell.requesting {
86+
case CompletionShell.bash:
87+
return "bash-specific script"
88+
case CompletionShell.fish:
89+
return "fish-specific script"
90+
case CompletionShell.zsh:
91+
return "zsh-specific script"
92+
default:
93+
// return a universal no-op for unknown shells
94+
return ":"
95+
}
96+
}
97+
98+
/// Runs during completion while user is typing command line to use your tool
99+
func generateCompletionCandidatesPerShell(_ arguments: [String]) -> [String] {
100+
switch CompletionShell.requesting {
101+
case CompletionShell.bash:
102+
return ["A:in:bash:syntax", "B:in:bash:syntax", "C:in:bash:syntax"]
103+
case CompletionShell.fish:
104+
return ["A:in:fish:syntax", "B:in:bash:syntax", "C:in:bash:syntax"]
105+
case CompletionShell.zsh:
106+
return ["A:in:zsh:syntax", "B:in:zsh:syntax", "C:in:zsh:syntax"]
107+
default:
108+
return []
109+
}
110+
}
111+
```

Sources/ArgumentParser/Parsing/CommandParser.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12+
#if swift(>=5.11)
13+
internal import class Foundation.ProcessInfo
14+
#elseif swift(>=5.10)
15+
import class Foundation.ProcessInfo
16+
#else
17+
@_implementationOnly import class Foundation.ProcessInfo
18+
#endif
19+
1220
struct CommandError: Error {
1321
var commandStack: [ParsableCommand.Type]
1422
var parserError: ParserError
@@ -356,6 +364,9 @@ extension CommandParser {
356364
// Parsing and retrieval successful! We don't want to continue with any
357365
// other parsing here, so after printing the result of the completion
358366
// function, exit with a success code.
367+
if let completionShellName = ProcessInfo.processInfo.environment[CompletionShell.environmentVariableName] {
368+
CompletionShell.requesting = CompletionShell(rawValue: completionShellName)
369+
}
359370
let output = completionFunction(completionValues).joined(separator: "\n")
360371
throw ParserError.completionScriptCustomResponse(output)
361372
}

Tests/ArgumentParserExampleTests/MathExampleTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ private let bashCompletionScriptText = """
241241
#!/bin/bash
242242
243243
_math() {
244+
export SAP_SHELL=bash
244245
cur="${COMP_WORDS[COMP_CWORD]}"
245246
prev="${COMP_WORDS[COMP_CWORD-1]}"
246247
COMPREPLY=()
@@ -395,6 +396,7 @@ _math_commandname=$words[1]
395396
typeset -A opt_args
396397
397398
_math() {
399+
export SAP_SHELL=zsh
398400
integer ret=1
399401
local -a args
400402
args+=(
@@ -583,6 +585,7 @@ function _swift_math_preprocessor
583585
end
584586
585587
function _swift_math_using_command
588+
set -x SAP_SHELL fish
586589
set -l currentCommands (_swift_math_preprocessor (commandline -opc))
587590
set -l expectedCommands (string split \" \" $argv[1])
588591
set -l subcommands (string split \" \" $argv[2])

Tests/ArgumentParserUnitTests/CompletionScriptTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ _base_test_commandname=$words[1]
177177
typeset -A opt_args
178178
179179
_base-test() {
180+
export SAP_SHELL=zsh
180181
integer ret=1
181182
local -a args
182183
args+=(
@@ -258,6 +259,7 @@ private let bashBaseCompletions = """
258259
#!/bin/bash
259260
260261
_base_test() {
262+
export SAP_SHELL=bash
261263
cur="${COMP_WORDS[COMP_CWORD]}"
262264
prev="${COMP_WORDS[COMP_CWORD-1]}"
263265
COMPREPLY=()
@@ -350,6 +352,7 @@ _escaped_command_commandname=$words[1]
350352
typeset -A opt_args
351353
352354
_escaped-command() {
355+
export SAP_SHELL=zsh
353356
integer ret=1
354357
local -a args
355358
args+=(
@@ -385,6 +388,7 @@ function _swift_base-test_preprocessor
385388
end
386389
387390
function _swift_base-test_using_command
391+
set -x SAP_SHELL fish
388392
set -l currentCommands (_swift_base-test_preprocessor (commandline -opc))
389393
set -l expectedCommands (string split " " $argv[1])
390394
set -l subcommands (string split " " $argv[2])
@@ -486,6 +490,7 @@ _parent_commandname=$words[1]
486490
typeset -A opt_args
487491
488492
_parent() {
493+
export SAP_SHELL=zsh
489494
integer ret=1
490495
local -a args
491496
args+=(
@@ -509,6 +514,7 @@ let bashHiddenCompletion = """
509514
#!/bin/bash
510515
511516
_parent() {
517+
export SAP_SHELL=bash
512518
cur="${COMP_WORDS[COMP_CWORD]}"
513519
prev="${COMP_WORDS[COMP_CWORD-1]}"
514520
COMPREPLY=()
@@ -538,6 +544,7 @@ function _swift_parent_preprocessor
538544
end
539545
540546
function _swift_parent_using_command
547+
set -x SAP_SHELL fish
541548
set -l currentCommands (_swift_parent_preprocessor (commandline -opc))
542549
set -l expectedCommands (string split " " $argv[1])
543550
set -l subcommands (string split " " $argv[2])

0 commit comments

Comments
 (0)