CompletionsGenerator.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. //===----------------------------------------------------------------------===//
  2. //
  3. // This source file is part of the Swift Argument Parser open source project
  4. //
  5. // Copyright (c) 2020 Apple Inc. and the Swift project authors
  6. // Licensed under Apache License v2.0 with Runtime Library Exception
  7. //
  8. // See https://swift.org/LICENSE.txt for license information
  9. //
  10. //===----------------------------------------------------------------------===//
  11. #if compiler(>=6.0)
  12. // import ArgumentParserToolInfo
  13. #else
  14. // import ArgumentParserToolInfo
  15. #endif
  16. /// A shell for which the parser can generate a completion script.
  17. public struct CompletionShell: RawRepresentable, Hashable, CaseIterable,
  18. Sendable
  19. {
  20. public var rawValue: String
  21. /// Creates a new instance from the given string.
  22. public init?(rawValue: String) {
  23. switch rawValue {
  24. case "zsh", "bash", "fish":
  25. self.rawValue = rawValue
  26. default:
  27. return nil
  28. }
  29. }
  30. /// An instance representing `zsh`.
  31. public static var zsh: CompletionShell {
  32. // swift-format-ignore: NeverForceUnwrap
  33. // Statically known valid raw value.
  34. CompletionShell(rawValue: "zsh")!
  35. }
  36. /// An instance representing `bash`.
  37. public static var bash: CompletionShell {
  38. // swift-format-ignore: NeverForceUnwrap
  39. // Statically known valid raw value.
  40. CompletionShell(rawValue: "bash")!
  41. }
  42. /// An instance representing `fish`.
  43. public static var fish: CompletionShell {
  44. // swift-format-ignore: NeverForceUnwrap
  45. // Statically known valid raw value.
  46. CompletionShell(rawValue: "fish")!
  47. }
  48. /// Returns an instance representing the current shell, if recognized.
  49. public static func autodetected() -> CompletionShell? {
  50. Platform.shellName.flatMap(CompletionShell.init(rawValue:))
  51. }
  52. /// An array of all supported shells for completion scripts.
  53. public static var allCases: [CompletionShell] {
  54. [.zsh, .bash, .fish]
  55. }
  56. static let _requesting = ArgParserMutex<CompletionShell?>(nil)
  57. /// The shell for which completions will be or are being requested.
  58. ///
  59. /// `CompletionShell.requesting` is non-`nil` only while generating a shell
  60. /// completion script or while a Swift custom completion function is executing
  61. /// to offer completions for a word from a command line (that is, while
  62. /// `customCompletion` from `@Option(completion: .custom(customCompletion))`
  63. /// executes).
  64. public static var requesting: CompletionShell? {
  65. Self._requesting.withLock { $0 }
  66. }
  67. static let _requestingVersion = ArgParserMutex<String?>(nil)
  68. /// The shell version for which completions will be or are being requested.
  69. ///
  70. /// `CompletionShell.requestingVersion` is non-`nil` only while generating a
  71. /// shell completion script or while a Swift custom completion function is
  72. /// running (that is, while `customCompletion` from
  73. /// `@Option(completion: .custom(customCompletion))` executes).
  74. public static var requestingVersion: String? {
  75. Self._requestingVersion.withLock { $0 }
  76. }
  77. func format(completions: [String]) -> String {
  78. var completions = completions
  79. if self == .zsh {
  80. // This pseudo-completion is removed by the zsh completion script.
  81. // It allows trailing empty string completions to work in zsh.
  82. // zsh completion scripts generated by older SAP versions ignore spaces.
  83. completions.append(" ")
  84. }
  85. return completions.joined(separator: "\n")
  86. }
  87. }
  88. struct CompletionsGenerator {
  89. var shell: CompletionShell
  90. var command: ParsableCommand.Type
  91. init(command: ParsableCommand.Type, shell: CompletionShell?) throws {
  92. guard let _shell = shell ?? .autodetected() else {
  93. throw ParserError.unsupportedShell()
  94. }
  95. self.shell = _shell
  96. self.command = command
  97. }
  98. init(command: ParsableCommand.Type, shellName: String?) throws {
  99. if let shellName = shellName {
  100. guard let shell = CompletionShell(rawValue: shellName) else {
  101. throw ParserError.unsupportedShell(shellName)
  102. }
  103. try self.init(command: command, shell: shell)
  104. } else {
  105. try self.init(command: command, shell: nil)
  106. }
  107. }
  108. /// Generates a shell completion script for this generator's shell and command.
  109. func generateCompletionScript() -> String {
  110. CompletionShell._requesting.withLock { $0 = shell }
  111. switch shell {
  112. case .zsh:
  113. return ToolInfoV0(commandStack: [command]).zshCompletionScript
  114. case .bash:
  115. return ToolInfoV0(commandStack: [command]).bashCompletionScript
  116. case .fish:
  117. return ToolInfoV0(commandStack: [command]).fishCompletionScript
  118. default:
  119. fatalError("Invalid CompletionShell: \(shell)")
  120. }
  121. }
  122. }
  123. extension String {
  124. func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self {
  125. iterationCount == 0
  126. ? self
  127. : self
  128. .replacing("'", with: "'\\''")
  129. .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1)
  130. }
  131. func shellEscapeForVariableName() -> Self {
  132. self.replacing("-", with: "_")
  133. }
  134. func replacing(_ old: Self, with new: Self) -> Self {
  135. guard !old.isEmpty else { return self }
  136. var result = ""
  137. var startIndex = self.startIndex
  138. // Look for occurrences of the old string.
  139. while let matchRange = self.firstMatch(of: old, at: startIndex) {
  140. // Add the substring before the match.
  141. result.append(contentsOf: self[startIndex..<matchRange.start])
  142. // Add the replacement string.
  143. result.append(contentsOf: new)
  144. // Move past the matched portion.
  145. startIndex = matchRange.end
  146. }
  147. // No more matches found, add the rest of the string.
  148. result.append(contentsOf: self[startIndex..<self.endIndex])
  149. return result
  150. }
  151. func firstMatch(
  152. of match: Self,
  153. at startIndex: Self.Index
  154. ) -> (start: Self.Index, end: Self.Index)? {
  155. guard !match.isEmpty else { return nil }
  156. guard match.count <= self.count else { return nil }
  157. var startIndex = startIndex
  158. while startIndex < self.endIndex {
  159. // Check if theres a match.
  160. if let endIndex = self.matches(match, at: startIndex) {
  161. // Return the match.
  162. return (startIndex, endIndex)
  163. }
  164. // Move to the next of index.
  165. self.formIndex(after: &startIndex)
  166. }
  167. return nil
  168. }
  169. func matches(
  170. _ match: Self,
  171. at startIndex: Self.Index
  172. ) -> Self.Index? {
  173. var selfIndex = startIndex
  174. var matchIndex = match.startIndex
  175. while true {
  176. // Only continue checking if there is more match to check
  177. guard matchIndex < match.endIndex else { return selfIndex }
  178. // Exit early if there is no more "self" to check.
  179. guard selfIndex < self.endIndex else { return nil }
  180. // Check match and self are the the same.
  181. guard self[selfIndex] == match[matchIndex] else { return nil }
  182. // Move to the next pair of indices.
  183. self.formIndex(after: &selfIndex)
  184. match.formIndex(after: &matchIndex)
  185. }
  186. }
  187. }
  188. extension CommandInfoV0 {
  189. var commandContext: [String] {
  190. (superCommands ?? []) + [commandName]
  191. }
  192. var initialCommand: String {
  193. superCommands?.first ?? commandName
  194. }
  195. var positionalArguments: [ArgumentInfoV0] {
  196. (arguments ?? []).filter { $0.kind == .positional }
  197. }
  198. var completionFunctionName: String {
  199. "_" + commandContext.joined(separator: "_")
  200. }
  201. var completionFunctionPrefix: String {
  202. "__\(initialCommand)"
  203. }
  204. }
  205. extension ArgumentInfoV0 {
  206. /// Returns a string with the arguments for the callback to generate custom
  207. /// completions for this argument.
  208. func commonCustomCompletionCall(command: CommandInfoV0) -> String {
  209. let subcommandNames =
  210. command.commandContext.dropFirst().map { "\($0) " }.joined()
  211. let argumentName: String
  212. switch kind {
  213. case .positional:
  214. if let index = command.positionalArguments.firstIndex(of: self) {
  215. argumentName = "positional@\(index)"
  216. } else {
  217. argumentName = "---"
  218. }
  219. default:
  220. argumentName = preferredName?.commonCompletionSynopsisString() ?? "---"
  221. }
  222. return "---completion \(subcommandNames)-- \(argumentName)"
  223. }
  224. }
  225. extension ArgumentInfoV0.NameInfoV0 {
  226. func commonCompletionSynopsisString() -> String {
  227. switch kind {
  228. case .long:
  229. return "--\(name)"
  230. case .short, .longWithSingleDash:
  231. return "-\(name)"
  232. }
  233. }
  234. }