BashCompletionsGenerator.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  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 bashCompletionScript: String {
  18. command.bashCompletionScript
  19. }
  20. }
  21. extension CommandInfoV0 {
  22. fileprivate var bashCompletionScript: String {
  23. """
  24. #!/bin/bash
  25. \(cursorIndexInCurrentWordFunctionName)() {
  26. local remaining="${COMP_LINE}"
  27. local word
  28. for word in "${COMP_WORDS[@]::COMP_CWORD}"; do
  29. remaining="${remaining##*([[:space:]])"${word}"*([[:space:]])}"
  30. done
  31. local -ir index="$((COMP_POINT - ${#COMP_LINE} + ${#remaining}))"
  32. if [[ "${index}" -le 0 ]]; then
  33. printf 0
  34. else
  35. printf %s "${index}"
  36. fi
  37. }
  38. # positional arguments:
  39. #
  40. # - 1: the current (sub)command's count of positional arguments
  41. #
  42. # required variables:
  43. #
  44. # - repeating_flags: the repeating flags that the current (sub)command can accept
  45. # - non_repeating_flags: the non-repeating flags that the current (sub)command can accept
  46. # - repeating_options: the repeating options that the current (sub)command can accept
  47. # - non_repeating_options: the non-repeating options that the current (sub)command can accept
  48. # - positional_number: value ignored
  49. # - unparsed_words: unparsed words from the current command line
  50. #
  51. # modified variables:
  52. #
  53. # - non_repeating_flags: remove flags for this (sub)command that are already on the command line
  54. # - non_repeating_options: remove options for this (sub)command that are already on the command line
  55. # - positional_number: set to the current positional number
  56. # - unparsed_words: remove all flags, options, and option values for this (sub)command
  57. \(offerFlagsOptionsFunctionName)() {
  58. local -ir positional_count="${1}"
  59. positional_number=0
  60. local was_flag_option_terminator_seen=false
  61. local is_parsing_option_value=false
  62. local -ar unparsed_word_indices=("${!unparsed_words[@]}")
  63. local -i word_index
  64. for word_index in "${unparsed_word_indices[@]}"; do
  65. if "${is_parsing_option_value}"; then
  66. # This word is an option value:
  67. # Reset marker for next word iff not currently the last word
  68. [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]] && is_parsing_option_value=false
  69. unset "unparsed_words[${word_index}]"
  70. # Do not process this word as a flag or an option
  71. continue
  72. fi
  73. local word="${unparsed_words["${word_index}"]}"
  74. if ! "${was_flag_option_terminator_seen}"; then
  75. case "${word}" in
  76. --)
  77. unset "unparsed_words[${word_index}]"
  78. # by itself -- is a flag/option terminator, but if it is the last word, it is the start of a completion
  79. if [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]]; then
  80. was_flag_option_terminator_seen=true
  81. fi
  82. continue
  83. ;;
  84. -*)
  85. # ${word} is a flag or an option
  86. # If ${word} is an option, mark that the next word to be parsed is an option value
  87. local option
  88. for option in "${repeating_options[@]}" "${non_repeating_options[@]}"; do
  89. [[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break
  90. done
  91. # Remove ${word} from ${non_repeating_flags} or ${non_repeating_options} so it isn't offered again
  92. local not_found=true
  93. local -i index
  94. for index in "${!non_repeating_flags[@]}"; do
  95. if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then
  96. unset "non_repeating_flags[${index}]"
  97. non_repeating_flags=("${non_repeating_flags[@]}")
  98. not_found=false
  99. break
  100. fi
  101. done
  102. if "${not_found}"; then
  103. for index in "${!non_repeating_flags[@]}"; do
  104. if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then
  105. unset "non_repeating_flags[${index}]"
  106. non_repeating_flags=("${non_repeating_flags[@]}")
  107. break
  108. fi
  109. done
  110. fi
  111. unset "unparsed_words[${word_index}]"
  112. continue
  113. ;;
  114. esac
  115. fi
  116. # ${word} is neither a flag, nor an option, nor an option value
  117. if [[ "${positional_number}" -lt "${positional_count}" || "${positional_count}" -lt 0 ]]; then
  118. # ${word} is a positional
  119. ((positional_number++))
  120. unset "unparsed_words[${word_index}]"
  121. else
  122. if [[ -z "${word}" ]]; then
  123. # Could be completing a flag, option, or subcommand
  124. positional_number=-1
  125. else
  126. # ${word} is a subcommand or invalid, so stop processing this (sub)command
  127. positional_number=-2
  128. fi
  129. break
  130. fi
  131. done
  132. unparsed_words=("${unparsed_words[@]}")
  133. if\\
  134. ! "${was_flag_option_terminator_seen}"\\
  135. && ! "${is_parsing_option_value}"\\
  136. && [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]]
  137. then
  138. COMPREPLY+=($(compgen -W "${repeating_flags[*]} ${non_repeating_flags[*]} ${repeating_options[*]} ${non_repeating_options[*]}" -- "${cur}"))
  139. fi
  140. }
  141. \(addCompletionsFunctionName)() {
  142. local completion
  143. while IFS='' read -r completion; do
  144. COMPREPLY+=("${completion}")
  145. done < <(IFS=$'\\n' compgen "${@}" -- "${cur}")
  146. }
  147. \(customCompleteFunctionName)() {
  148. if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then
  149. local -ar words=("${COMP_WORDS[@]}")
  150. else
  151. local -ar words=("${COMP_WORDS[@]::${COMP_CWORD}}" '' "${COMP_WORDS[@]:${COMP_CWORD}}")
  152. fi
  153. "${COMP_WORDS[0]}" "${@}" "${words[@]}"
  154. }
  155. \(completionFunctions)\
  156. complete -o filenames -F \(completionFunctionName) \(commandName)
  157. """
  158. }
  159. /// Generates a Bash completion function.
  160. private var completionFunctions: String {
  161. let functionName = completionFunctionName
  162. let subcommands = (subcommands ?? []).filter(\.shouldDisplay)
  163. // Start building the resulting function code.
  164. var result = ""
  165. // Include initial setup iff the root command.
  166. let declareTopLevelArray: String
  167. if (superCommands ?? []).isEmpty {
  168. result += """
  169. local state
  170. state="$(shopt -p;shopt -po)"
  171. trap "${state//$'\\n'/;}" RETURN
  172. shopt -s extglob
  173. set +o history +o posix
  174. local -xr \(Platform.Environment.Key.shellName.rawValue)=bash
  175. local -x \(Platform.Environment.Key.shellVersion.rawValue)
  176. \(Platform.Environment.Key.shellVersion.rawValue)="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")"
  177. local -r \(Platform.Environment.Key.shellVersion.rawValue)
  178. local -r cur="${2}"
  179. local -r prev="${3}"
  180. local -i positional_number
  181. local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}")
  182. """
  183. declareTopLevelArray = "local -a "
  184. } else {
  185. declareTopLevelArray = ""
  186. }
  187. let positionalArguments = positionalArguments
  188. let arguments = arguments ?? []
  189. let flags = arguments.filter { $0.kind == .flag }
  190. let options = arguments.filter { $0.kind == .option }
  191. if !flags.flatMap(\.completionWords).isEmpty
  192. || !options.flatMap(\.completionWords).isEmpty
  193. {
  194. result += """
  195. \(declareTopLevelArray)repeating_flags=(\(flags.filter(\.isRepeating).flatMap(\.completionWords).joined(separator: " ")))
  196. \(declareTopLevelArray)non_repeating_flags=(\(flags.filter { !$0.isRepeating }.flatMap(\.completionWords).joined(separator: " ")))
  197. \(declareTopLevelArray)repeating_options=(\(options.filter(\.isRepeating).flatMap(\.completionWords).joined(separator: " ")))
  198. \(declareTopLevelArray)non_repeating_options=(\(options.filter { !$0.isRepeating }.flatMap(\.completionWords).joined(separator: " ")))
  199. \(offerFlagsOptionsFunctionName) \
  200. \(positionalArguments.contains { $0.isRepeating } ? -1 : positionalArguments.count)
  201. """
  202. }
  203. // Generate the case pattern-matching statements for option values.
  204. // If there aren't any, skip the case block altogether.
  205. let optionHandlers =
  206. options.compactMap { arg in
  207. guard arg.kind != .flag else { return nil }
  208. let words = arg.completionWords
  209. guard !words.isEmpty else { return nil }
  210. return """
  211. \(words.map { "'\($0.shellEscapeForSingleQuotedString())'" }.joined(separator: "|")))
  212. \(valueCompletion(arg).indentingEachLine(by: 8))\
  213. return
  214. ;;
  215. """
  216. }
  217. .joined(separator: "\n")
  218. if !optionHandlers.isEmpty {
  219. result += """
  220. # Offer option value completions
  221. case "${prev}" in
  222. \(optionHandlers)
  223. esac
  224. """
  225. }
  226. var encounteredRepeatingPositional = false
  227. let positionalCases =
  228. zip(1..., positionalArguments)
  229. .compactMap { position, arg in
  230. guard !encounteredRepeatingPositional else {
  231. return nil as String?
  232. }
  233. if arg.isRepeating {
  234. encounteredRepeatingPositional = true
  235. }
  236. let completion = valueCompletion(arg)
  237. return completion.isEmpty
  238. ? nil
  239. : """
  240. \(encounteredRepeatingPositional ? "*" : position.description))
  241. \(completion.indentingEachLine(by: 8))\
  242. return
  243. ;;
  244. """
  245. }
  246. if !positionalCases.isEmpty {
  247. result += """
  248. # Offer positional completions
  249. case "${positional_number}" in
  250. \(positionalCases.joined())\
  251. esac
  252. """
  253. }
  254. if !subcommands.isEmpty {
  255. result += """
  256. # Offer subcommand / subcommand argument completions
  257. local -r subcommand="${unparsed_words[0]}"
  258. unset 'unparsed_words[0]'
  259. unparsed_words=("${unparsed_words[@]}")
  260. case "${subcommand}" in
  261. \(subcommands.map(\.commandName).joined(separator: "|")))
  262. # Offer subcommand argument completions
  263. "\(functionName)_${subcommand}"
  264. ;;
  265. *)
  266. # Offer subcommand completions
  267. COMPREPLY+=($(compgen -W '\(
  268. subcommands.map { $0.commandName.shellEscapeForSingleQuotedString() }.joined(separator: " ")
  269. )' -- "${cur}"))
  270. ;;
  271. esac
  272. """
  273. }
  274. if result.isEmpty {
  275. result = " :\n"
  276. }
  277. return """
  278. \(functionName)() {
  279. \(result)\
  280. }
  281. \(subcommands.map(\.completionFunctions).joined())
  282. """
  283. }
  284. /// Returns the completions that can follow the given argument's `--name`.
  285. private func valueCompletion(_ arg: ArgumentInfoV0) -> String {
  286. switch arg.completionKind {
  287. case .none:
  288. return ""
  289. case .file(let extensions) where extensions.isEmpty:
  290. return """
  291. \(addCompletionsFunctionName) -f
  292. """
  293. case .file(let extensions):
  294. let exts =
  295. extensions
  296. .map { $0.shellEscapeForSingleQuotedString() }.joined(separator: "|")
  297. return """
  298. \(addCompletionsFunctionName) -o plusdirs -fX '!*.@(\(exts))'
  299. """
  300. case .directory:
  301. return """
  302. \(addCompletionsFunctionName) -d
  303. """
  304. case .list(let list):
  305. return """
  306. \(addCompletionsFunctionName) -W\
  307. '\(list.map { $0.shellEscapeForSingleQuotedString() }.joined(separator: "'$'\\n''"))'
  308. """
  309. case .shellCommand(let command):
  310. return """
  311. \(addCompletionsFunctionName) -W "$(eval '\(command.shellEscapeForSingleQuotedString())')"
  312. """
  313. case .custom, .customAsync:
  314. return """
  315. \(addCompletionsFunctionName) -W\
  316. "$(\(customCompleteFunctionName) \(arg.commonCustomCompletionCall(command: self))\
  317. "${COMP_CWORD}"\
  318. "$(\(cursorIndexInCurrentWordFunctionName))")"
  319. """
  320. case .customDeprecated:
  321. return """
  322. \(addCompletionsFunctionName) -W\
  323. "$(\(customCompleteFunctionName) \(arg.commonCustomCompletionCall(command: self)))"
  324. """
  325. }
  326. }
  327. private var cursorIndexInCurrentWordFunctionName: String {
  328. "\(completionFunctionPrefix)_cursor_index_in_current_word"
  329. }
  330. private var offerFlagsOptionsFunctionName: String {
  331. "\(completionFunctionPrefix)_offer_flags_options"
  332. }
  333. private var addCompletionsFunctionName: String {
  334. "\(completionFunctionPrefix)_add_completions"
  335. }
  336. private var customCompleteFunctionName: String {
  337. "\(completionFunctionPrefix)_custom_complete"
  338. }
  339. }
  340. extension ArgumentInfoV0 {
  341. /// Returns the different completion names for this argument.
  342. fileprivate var completionWords: [String] {
  343. shouldDisplay
  344. ? (names ?? []).map { $0.commonCompletionSynopsisString() }
  345. : []
  346. }
  347. }