Skip to content

Commit 5323d56

Browse files
authored
Escape backslash and square brackets in addition to single quotes (#222)
* Escape backslash and square brackets as well as single quotes (#218 and #221) - Escape appropriate chars in subcommand abstracts as well. * Add test for escaped zsh characters
1 parent 163211e commit 5323d56

File tree

2 files changed

+61
-5
lines changed

2 files changed

+61
-5
lines changed

Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ struct ZshCompletionsGenerator {
5050

5151
let subcommandModes = subcommands.map {
5252
"""
53-
'\($0._commandName):\($0.configuration.abstract)'
53+
'\($0._commandName):\($0.configuration.abstract.zshEscaped())'
5454
"""
5555
.indentingEachLine(by: 12)
5656
}
@@ -112,8 +112,15 @@ struct ZshCompletionsGenerator {
112112

113113
extension String {
114114
fileprivate func zshEscapingSingleQuotes() -> String {
115-
self.split(separator: "'", omittingEmptySubsequences: false)
116-
.joined(separator: #"'"'"'"#)
115+
self.replacingOccurrences(of: "'", with: #"'"'"'"#)
116+
}
117+
118+
fileprivate func zshEscapingMetacharacters() -> String {
119+
self.replacingOccurrences(of: #"[\\\[\]]"#, with: #"\\$0"#, options: .regularExpression)
120+
}
121+
122+
fileprivate func zshEscaped() -> String {
123+
self.zshEscapingSingleQuotes().zshEscapingMetacharacters()
117124
}
118125
}
119126

@@ -123,7 +130,7 @@ extension ArgumentDefinition {
123130
let abstract = help.help?.abstract,
124131
!abstract.isEmpty
125132
else { return "" }
126-
return "[\(abstract.zshEscapingSingleQuotes())]"
133+
return "[\(abstract.zshEscaped())]"
127134
}
128135

129136
func zshCompletionString(_ commands: [ParsableCommand.Type]) -> String? {
@@ -165,7 +172,7 @@ extension ArgumentDefinition {
165172
let pattern = extensions.isEmpty
166173
? ""
167174
: " -g '\(extensions.map { "*." + $0 }.joined(separator: " "))'"
168-
return "_files\(pattern.zshEscapingSingleQuotes())"
175+
return "_files\(pattern.zshEscaped())"
169176

170177
case .directory:
171178
return "_files -/"

Tests/ArgumentParserUnitTests/CompletionScriptTests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ extension CompletionScriptTests {
109109
}
110110
}
111111

112+
extension CompletionScriptTests {
113+
struct Escaped: ParsableCommand {
114+
@Option(help: #"Escaped chars: '[]\."#)
115+
var one: String
116+
}
117+
118+
func testEscaped_Zsh() throws {
119+
let script1 = try CompletionsGenerator(command: Escaped.self, shell: .zsh)
120+
.generateCompletionScript()
121+
XCTAssertEqual(zshEscapedCompletion, script1)
122+
123+
let script2 = try CompletionsGenerator(command: Escaped.self, shellName: "zsh")
124+
.generateCompletionScript()
125+
XCTAssertEqual(zshEscapedCompletion, script2)
126+
127+
let script3 = Escaped.completionScript(for: .zsh)
128+
XCTAssertEqual(zshEscapedCompletion, script3)
129+
}
130+
}
131+
112132
private let zshBaseCompletions = """
113133
#compdef base
114134
local context state state_descr line
@@ -185,3 +205,32 @@ _base() {
185205
186206
complete -F _base base
187207
"""
208+
209+
private let zshEscapedCompletion = """
210+
#compdef escaped
211+
local context state state_descr line
212+
_escaped_commandname=$words[1]
213+
typeset -A opt_args
214+
215+
_escaped() {
216+
integer ret=1
217+
local -a args
218+
args+=(
219+
'--one[Escaped chars: '"'"'\\[\\]\\\\.]:one:'
220+
'(-h --help)'{-h,--help}'[Print help information.]'
221+
)
222+
_arguments -w -s -S $args[@] && ret=0
223+
224+
return ret
225+
}
226+
227+
228+
_custom_completion() {
229+
local completions=($($*))
230+
_describe '' completions
231+
}
232+
233+
_escaped
234+
"""
235+
236+

0 commit comments

Comments
 (0)