FishCompletionsGenerator.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  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. extension ToolInfoV0 {
  17. var fishCompletionScript: String {
  18. command.fishCompletionScript
  19. }
  20. }
  21. extension CommandInfoV0 {
  22. fileprivate var fishCompletionScript: String {
  23. """
  24. function \(shouldOfferCompletionsForFlagsOrOptionsFunctionName) -a expected_commands
  25. set -l non_repeating_flags_or_options $argv[2..]
  26. set -l non_repeating_flags_or_options_absent 0
  27. set -l positional_index 0
  28. set -l commands
  29. \(parseTokensFunctionName)
  30. test "$commands" = "$expected_commands"; and return $non_repeating_flags_or_options_absent
  31. end
  32. function \(shouldOfferCompletionsForPositionalFunctionName) -a expected_commands expected_positional_index positional_index_comparison
  33. if test -z $positional_index_comparison
  34. set positional_index_comparison -eq
  35. end
  36. set -l non_repeating_flags_or_options
  37. set -l non_repeating_flags_or_options_absent 0
  38. set -l positional_index 0
  39. set -l commands
  40. \(parseTokensFunctionName)
  41. test "$commands" = "$expected_commands" -a \\( "$positional_index" "$positional_index_comparison" "$expected_positional_index" \\)
  42. end
  43. function \(parseTokensFunctionName) -S
  44. set -l unparsed_tokens (\(tokensFunctionName) -pc)
  45. set -l present_flags_and_options
  46. switch $unparsed_tokens[1]
  47. \(commandCases)
  48. end
  49. end
  50. function \(tokensFunctionName)
  51. if test (string split -m 1 -f 1 -- . "$FISH_VERSION") -gt 3
  52. commandline --tokens-raw $argv
  53. else
  54. commandline -o $argv
  55. end
  56. end
  57. function \(parseSubcommandFunctionName) -S -a positional_count
  58. argparse -s r -- $argv
  59. set -l option_specs $argv[2..]
  60. set -a commands $unparsed_tokens[1]
  61. set -e unparsed_tokens[1]
  62. set positional_index 0
  63. while true
  64. argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null
  65. set unparsed_tokens $argv
  66. set positional_index (math $positional_index + 1)
  67. for non_repeating_flag_or_option in $non_repeating_flags_or_options
  68. if set -ql _flag_$non_repeating_flag_or_option
  69. set non_repeating_flags_or_options_absent 1
  70. break
  71. end
  72. end
  73. if test (count $unparsed_tokens) -eq 0 -o \\( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \\)
  74. break
  75. end
  76. set -e unparsed_tokens[1]
  77. end
  78. end
  79. function \(completeDirectoriesFunctionName)
  80. set -l token (commandline -t)
  81. string match -- '*/' $token
  82. set -l subdirs $token*/
  83. printf '%s\\n' $subdirs
  84. end
  85. function \(customCompletionFunctionName)
  86. set -x \(Platform.Environment.Key.shellName.rawValue) fish
  87. set -x \(Platform.Environment.Key.shellVersion.rawValue) $FISH_VERSION
  88. set -l tokens (\(tokensFunctionName) -p)
  89. if test -z (\(tokensFunctionName) -t)
  90. set -l index (count (\(tokensFunctionName) -pc))
  91. set tokens $tokens[..$index] \\'\\' $tokens[(math $index + 1)..]
  92. end
  93. command $tokens[1] $argv $tokens
  94. end
  95. complete -c '\(commandName)' -f
  96. \(completions.joined(separator: "\n"))
  97. """
  98. }
  99. private var commandCases: String {
  100. let subcommands = (subcommands ?? []).filter(\.shouldDisplay)
  101. return """
  102. case '\(commandName)'
  103. \(parseSubcommandFunctionName) \(positionalArgumentCountArguments) \(
  104. completableArguments
  105. .compactMap(\.optionSpec)
  106. .map { "'\($0.fishEscapeForSingleQuotedString())'" }
  107. .joined(separator: separator)
  108. )\(
  109. subcommands.isEmpty
  110. ? ""
  111. : """
  112. switch $unparsed_tokens[1]
  113. \(subcommands.map(\.commandCases).joined(separator: "\n"))
  114. end
  115. """
  116. )
  117. """
  118. .indentingEachLine(by: 4)
  119. }
  120. private var completions: [String] {
  121. let prefix = "complete -c '\(initialCommand)' -n '"
  122. let subcommands = (subcommands ?? []).filter(\.shouldDisplay)
  123. var positionalIndex = 0
  124. var repeatingPositionalComparison = ""
  125. let argumentCompletions =
  126. completableArguments
  127. .compactMap { arg in
  128. if arg.kind == .positional {
  129. guard repeatingPositionalComparison.isEmpty else {
  130. return nil as String?
  131. }
  132. if arg.isRepeating {
  133. repeatingPositionalComparison = " -ge"
  134. }
  135. }
  136. return """
  137. \(prefix)\(
  138. arg.kind == .positional
  139. ? """
  140. \(shouldOfferCompletionsForPositionalFunctionName) "\(commandContext.joined(separator: separator))" \({
  141. positionalIndex += 1
  142. return "\(positionalIndex)\(repeatingPositionalComparison)"
  143. }())
  144. """
  145. : """
  146. \(shouldOfferCompletionsForFlagsOrOptionsFunctionName) "\(commandContext.joined(separator: separator))"\
  147. \((arg.isRepeating ? [] : arg.names ?? []).map { " \($0.name)" }.sorted().joined())
  148. """
  149. )' \(argumentSegments(arg).joined(separator: separator))
  150. """
  151. }
  152. positionalIndex += 1
  153. return
  154. argumentCompletions
  155. + subcommands.map {
  156. """
  157. \(prefix)\(shouldOfferCompletionsForPositionalFunctionName) "\(commandContext.joined(separator: separator))"\
  158. \(positionalIndex)' -fa '\($0.commandName)' -d '\($0.abstract?.fishEscapeForSingleQuotedString() ?? "")'
  159. """
  160. }
  161. + subcommands.flatMap(\.completions)
  162. }
  163. private var completableArguments: [ArgumentInfoV0] {
  164. (arguments ?? []).compactMap { arg in
  165. switch arg.completionKind {
  166. case .none where arg.names?.isEmpty ?? true:
  167. return nil
  168. default:
  169. return
  170. arg.shouldDisplay
  171. ? arg
  172. : nil
  173. }
  174. }
  175. }
  176. private func argumentSegments(_ arg: ArgumentInfoV0) -> [String] {
  177. var results: [String] = []
  178. if let names = arg.names, !names.isEmpty {
  179. results += names.map(\.asCompleteArgument)
  180. if let abstract = arg.abstract, !abstract.isEmpty {
  181. results += [
  182. "-d '\(abstract.fishEscapeForSingleQuotedString())'"
  183. ]
  184. }
  185. }
  186. let r = arg.kind == .positional ? "" : "r"
  187. switch arg.completionKind {
  188. case .none:
  189. switch arg.kind {
  190. case .positional,
  191. .option:
  192. results += ["-\(r)fka ''"]
  193. default:
  194. break
  195. }
  196. break
  197. case .list(let list):
  198. results += ["-\(r)fka '\(list.joined(separator: separator))'"]
  199. case .file(let extensions):
  200. switch extensions.count {
  201. case 0:
  202. results += ["-\(r)F"]
  203. case 1:
  204. results += [
  205. """
  206. -\(r)fa '(\
  207. for p in (string match -e -- \\'*/\\' (commandline -t);or printf \\n)*.\\'\(extensions.map { $0.fishEscapeForSingleQuotedString(iterationCount: 2) }.joined())\\';printf %s\\n $p;end;\
  208. __fish_complete_directories (commandline -t) \\'\\'\
  209. )'
  210. """
  211. ]
  212. default:
  213. results += [
  214. """
  215. -\(r)fa '(\
  216. set -l exts \(extensions.map { "\\'\($0.fishEscapeForSingleQuotedString(iterationCount: 2))\\'" }.joined(separator: separator));\
  217. for p in (string match -e -- \\'*/\\' (commandline -t);or printf \\n)*.{$exts};printf %s\\n $p;end;\
  218. __fish_complete_directories (commandline -t) \\'\\'\
  219. )'
  220. """
  221. ]
  222. }
  223. case .directory:
  224. results += ["-\(r)fa '(\(completeDirectoriesFunctionName))'"]
  225. case .shellCommand(let shellCommand):
  226. results += [
  227. "-\(r)fka '(\(shellCommand.fishEscapeForSingleQuotedString()))'"
  228. ]
  229. case .custom, .customAsync:
  230. results += [
  231. """
  232. -\(r)fka '(\
  233. \(customCompletionFunctionName) \(arg.commonCustomCompletionCall(command: self)) \
  234. (count (\(tokensFunctionName) -pc)) (\(tokensFunctionName) -tC)\
  235. )'
  236. """
  237. ]
  238. case .customDeprecated:
  239. results += [
  240. """
  241. -\(r)fka '(\(customCompletionFunctionName) \(arg.commonCustomCompletionCall(command: self)))'
  242. """
  243. ]
  244. }
  245. return results
  246. }
  247. var positionalArgumentCountArguments: String {
  248. let positionalArguments = positionalArguments
  249. return """
  250. \(positionalArguments.contains(where: { $0.isRepeating }) ? "-r " : "")\(positionalArguments.count)
  251. """
  252. }
  253. private var shouldOfferCompletionsForFlagsOrOptionsFunctionName: String {
  254. "\(completionFunctionPrefix)_should_offer_completions_for_flags_or_options"
  255. }
  256. private var shouldOfferCompletionsForPositionalFunctionName: String {
  257. "\(completionFunctionPrefix)_should_offer_completions_for_positional"
  258. }
  259. private var parseTokensFunctionName: String {
  260. "\(completionFunctionPrefix)_parse_tokens"
  261. }
  262. private var tokensFunctionName: String {
  263. "\(completionFunctionPrefix)_tokens"
  264. }
  265. private var parseSubcommandFunctionName: String {
  266. "\(completionFunctionPrefix)_parse_subcommand"
  267. }
  268. private var completeDirectoriesFunctionName: String {
  269. "\(completionFunctionPrefix)_complete_directories"
  270. }
  271. private var customCompletionFunctionName: String {
  272. "\(completionFunctionPrefix)_custom_completion"
  273. }
  274. }
  275. extension ArgumentInfoV0 {
  276. fileprivate var optionSpec: String? {
  277. guard let shortName = name(.short) else {
  278. guard let longName = name(.long) else {
  279. return nil
  280. }
  281. return optionSpecRequiresValue(longName)
  282. }
  283. guard let longName = name(.long) else {
  284. return optionSpecRequiresValue(shortName)
  285. }
  286. return optionSpecRequiresValue("\(shortName)/\(longName)")
  287. }
  288. private func name(_ nameKind: NameInfoV0.KindV0) -> String? {
  289. (names ?? []).first(where: { $0.kind == nameKind })?.name
  290. }
  291. private func optionSpecRequiresValue(_ optionSpec: String) -> String {
  292. switch kind {
  293. case .option:
  294. return "\(optionSpec)=\(isRepeating ? "+" : "")"
  295. default:
  296. return optionSpec
  297. }
  298. }
  299. }
  300. extension ArgumentInfoV0.NameInfoV0 {
  301. fileprivate var asCompleteArgument: String {
  302. switch kind {
  303. case .long:
  304. return "-l '\(name.fishEscapeForSingleQuotedString())'"
  305. case .short:
  306. return "-s '\(name.fishEscapeForSingleQuotedString())'"
  307. case .longWithSingleDash:
  308. return "-o '\(name.fishEscapeForSingleQuotedString())'"
  309. }
  310. }
  311. }
  312. extension String {
  313. fileprivate func fishEscapeForSingleQuotedString(
  314. iterationCount: UInt64 = 1
  315. ) -> Self {
  316. iterationCount == 0
  317. ? self
  318. : self
  319. .replacing("\\", with: "\\\\")
  320. .replacing("'", with: "\\'")
  321. .fishEscapeForSingleQuotedString(iterationCount: iterationCount - 1)
  322. }
  323. }
  324. private var separator: String { " " }