Skip to content

Commit f2eda39

Browse files
authored
Improve zsh completion script generation (#727)
* Do not indent zsh cases. Simplify zsh indent generation. * Do not prefix zsh cases with an open parenthesis. * Prevent zsh parameter word splitting. Brace & quote parameter uses. Use [@] for quoted array output. * Improve comment in ZshCompletionsGenerator.swift. * 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. * Remove extraneous zsh newline. * Improve zsh variable declarations: scoping, typing & readonly. Remove trailing spaces from InstallingCompletionScripts.md. * Include zsh words before current subcommand in custom completion arg. * Make subcommandHandler in ZshCompletionsGenerator.swift immutable. * Escape zsh single quotes via '\'' instead of via '"'"'. * 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. * 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. * Simplify zsh subcommand completion function dispatch. * Restrict access to symbols in ZshCompletionsGenerator.swift. * Add default help to zsh completions iff no existing help subcommand. * Use interpolated Strings in ZshCompletionsGenerator.swift. * Create & use zsh __completion function. * Improve zsh escaping. * Set zsh settings to a known state. Disable history ! in zsh completion scripts. * Inline single-use functions & variables in ZshCompletionsGenerator.swift. * Overhaul ZshCompletionsGenerator.swift as [ParsableCommand.Type] extension. * Move functions in ZshCompletionsGenerator.swift. Move from ArgumentDefinition extension to [ParsableCommand.Type] extension. * Move zsh helper functions before command functions to mirror other shells. * 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. * Separate zsh _arguments flags from specs using :. * Rename zsh args variable as arg_specs. * Simplify zshCompletionString(…). * Allow generating zsh setup scripts for arguments. * Use zsh array for list completions instead of nested strings. Allows list completions to contain spaces. Resolve #726 * Make CompletionShell.format(…) internal instead of public. * Reword uses of "iff" in completions code. Redid a comment as a DocC. * 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. * 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>
1 parent 385cfb2 commit f2eda39

22 files changed

+536
-373
lines changed

Sources/ArgumentParser/Completions/CompletionsGenerator.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,17 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable {
9898
///
9999
/// The environment variable is set in generated completion scripts.
100100
static let shellVersionEnvironmentVariableName = "SAP_SHELL_VERSION"
101+
102+
func format(completions: [String]) -> String {
103+
var completions = completions
104+
if self == .zsh {
105+
// This pseudo-completion is removed by the zsh completion script.
106+
// It allows trailing empty string completions to work in zsh.
107+
// zsh completion scripts generated by older SAP versions ignore spaces.
108+
completions.append(" ")
109+
}
110+
return completions.joined(separator: "\n")
111+
}
101112
}
102113

103114
struct CompletionsGenerator {
@@ -129,7 +140,7 @@ struct CompletionsGenerator {
129140
CompletionShell._requesting.withLock { $0 = shell }
130141
switch shell {
131142
case .zsh:
132-
return ZshCompletionsGenerator.generateCompletionScript(command)
143+
return [command].zshCompletionScript
133144
case .bash:
134145
return BashCompletionsGenerator.generateCompletionScript(command)
135146
case .fish:
@@ -164,11 +175,40 @@ extension ParsableCommand {
164175
}
165176
}
166177

178+
extension [ParsableCommand.Type] {
179+
/// Include default 'help' subcommand in nonempty subcommand list if & only if
180+
/// no help subcommand already exists.
181+
mutating func addHelpSubcommandIfMissing() {
182+
if !isEmpty && allSatisfy({ $0._commandName != "help" }) {
183+
append(HelpCommand.self)
184+
}
185+
}
186+
}
187+
167188
extension Sequence where Element == ParsableCommand.Type {
168189
func completionFunctionName() -> String {
169190
"_"
170191
+ self.flatMap { $0.compositeCommandName }
171192
.uniquingAdjacentElements()
172193
.joined(separator: "_")
173194
}
195+
196+
var shellVariableNamePrefix: String {
197+
flatMap { $0.compositeCommandName }
198+
.joined(separator: "_")
199+
.shellEscapeForVariableName()
200+
}
201+
}
202+
203+
extension String {
204+
func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self {
205+
iterationCount == 0
206+
? self
207+
: replacingOccurrences(of: "'", with: "'\\''")
208+
.shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1)
209+
}
210+
211+
func shellEscapeForVariableName() -> Self {
212+
replacingOccurrences(of: "-", with: "_")
213+
}
174214
}

0 commit comments

Comments
 (0)