| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- //===----------------------------------------------------------------------===//
- //
- // This source file is part of the Swift Argument Parser open source project
- //
- // Copyright (c) 2020 Apple Inc. and the Swift project authors
- // Licensed under Apache License v2.0 with Runtime Library Exception
- //
- // See https://swift.org/LICENSE.txt for license information
- //
- //===----------------------------------------------------------------------===//
- #if compiler(>=6.0)
- // import ArgumentParserToolInfo
- #else
- // import ArgumentParserToolInfo
- #endif
- /// A shell for which the parser can generate a completion script.
- public struct CompletionShell: RawRepresentable, Hashable, CaseIterable,
- Sendable
- {
- public var rawValue: String
- /// Creates a new instance from the given string.
- public init?(rawValue: String) {
- switch rawValue {
- case "zsh", "bash", "fish":
- self.rawValue = rawValue
- default:
- return nil
- }
- }
- /// An instance representing `zsh`.
- public static var zsh: CompletionShell {
- // swift-format-ignore: NeverForceUnwrap
- // Statically known valid raw value.
- CompletionShell(rawValue: "zsh")!
- }
- /// An instance representing `bash`.
- public static var bash: CompletionShell {
- // swift-format-ignore: NeverForceUnwrap
- // Statically known valid raw value.
- CompletionShell(rawValue: "bash")!
- }
- /// An instance representing `fish`.
- public static var fish: CompletionShell {
- // swift-format-ignore: NeverForceUnwrap
- // Statically known valid raw value.
- CompletionShell(rawValue: "fish")!
- }
- /// Returns an instance representing the current shell, if recognized.
- public static func autodetected() -> CompletionShell? {
- Platform.shellName.flatMap(CompletionShell.init(rawValue:))
- }
- /// An array of all supported shells for completion scripts.
- public static var allCases: [CompletionShell] {
- [.zsh, .bash, .fish]
- }
- static let _requesting = ArgParserMutex<CompletionShell?>(nil)
- /// The shell for which completions will be or are being requested.
- ///
- /// `CompletionShell.requesting` is non-`nil` only while generating a shell
- /// completion script or while a Swift custom completion function is executing
- /// to offer completions for a word from a command line (that is, while
- /// `customCompletion` from `@Option(completion: .custom(customCompletion))`
- /// executes).
- public static var requesting: CompletionShell? {
- Self._requesting.withLock { $0 }
- }
- static let _requestingVersion = ArgParserMutex<String?>(nil)
- /// The shell version for which completions will be or are being requested.
- ///
- /// `CompletionShell.requestingVersion` is non-`nil` only while generating a
- /// shell completion script or while a Swift custom completion function is
- /// running (that is, while `customCompletion` from
- /// `@Option(completion: .custom(customCompletion))` executes).
- public static var requestingVersion: String? {
- Self._requestingVersion.withLock { $0 }
- }
- func format(completions: [String]) -> String {
- var completions = completions
- if self == .zsh {
- // This pseudo-completion is removed by the zsh completion script.
- // It allows trailing empty string completions to work in zsh.
- // zsh completion scripts generated by older SAP versions ignore spaces.
- completions.append(" ")
- }
- return completions.joined(separator: "\n")
- }
- }
- struct CompletionsGenerator {
- var shell: CompletionShell
- var command: ParsableCommand.Type
- init(command: ParsableCommand.Type, shell: CompletionShell?) throws {
- guard let _shell = shell ?? .autodetected() else {
- throw ParserError.unsupportedShell()
- }
- self.shell = _shell
- self.command = command
- }
- init(command: ParsableCommand.Type, shellName: String?) throws {
- if let shellName = shellName {
- guard let shell = CompletionShell(rawValue: shellName) else {
- throw ParserError.unsupportedShell(shellName)
- }
- try self.init(command: command, shell: shell)
- } else {
- try self.init(command: command, shell: nil)
- }
- }
- /// Generates a shell completion script for this generator's shell and command.
- func generateCompletionScript() -> String {
- CompletionShell._requesting.withLock { $0 = shell }
- switch shell {
- case .zsh:
- return ToolInfoV0(commandStack: [command]).zshCompletionScript
- case .bash:
- return ToolInfoV0(commandStack: [command]).bashCompletionScript
- case .fish:
- return ToolInfoV0(commandStack: [command]).fishCompletionScript
- default:
- fatalError("Invalid CompletionShell: \(shell)")
- }
- }
- }
- extension String {
- func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self {
- iterationCount == 0
- ? self
- : self
- .replacing("'", with: "'\\''")
- .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1)
- }
- func shellEscapeForVariableName() -> Self {
- self.replacing("-", with: "_")
- }
- func replacing(_ old: Self, with new: Self) -> Self {
- guard !old.isEmpty else { return self }
- var result = ""
- var startIndex = self.startIndex
- // Look for occurrences of the old string.
- while let matchRange = self.firstMatch(of: old, at: startIndex) {
- // Add the substring before the match.
- result.append(contentsOf: self[startIndex..<matchRange.start])
- // Add the replacement string.
- result.append(contentsOf: new)
- // Move past the matched portion.
- startIndex = matchRange.end
- }
- // No more matches found, add the rest of the string.
- result.append(contentsOf: self[startIndex..<self.endIndex])
- return result
- }
- func firstMatch(
- of match: Self,
- at startIndex: Self.Index
- ) -> (start: Self.Index, end: Self.Index)? {
- guard !match.isEmpty else { return nil }
- guard match.count <= self.count else { return nil }
- var startIndex = startIndex
- while startIndex < self.endIndex {
- // Check if theres a match.
- if let endIndex = self.matches(match, at: startIndex) {
- // Return the match.
- return (startIndex, endIndex)
- }
- // Move to the next of index.
- self.formIndex(after: &startIndex)
- }
- return nil
- }
- func matches(
- _ match: Self,
- at startIndex: Self.Index
- ) -> Self.Index? {
- var selfIndex = startIndex
- var matchIndex = match.startIndex
- while true {
- // Only continue checking if there is more match to check
- guard matchIndex < match.endIndex else { return selfIndex }
- // Exit early if there is no more "self" to check.
- guard selfIndex < self.endIndex else { return nil }
- // Check match and self are the the same.
- guard self[selfIndex] == match[matchIndex] else { return nil }
- // Move to the next pair of indices.
- self.formIndex(after: &selfIndex)
- match.formIndex(after: &matchIndex)
- }
- }
- }
- extension CommandInfoV0 {
- var commandContext: [String] {
- (superCommands ?? []) + [commandName]
- }
- var initialCommand: String {
- superCommands?.first ?? commandName
- }
- var positionalArguments: [ArgumentInfoV0] {
- (arguments ?? []).filter { $0.kind == .positional }
- }
- var completionFunctionName: String {
- "_" + commandContext.joined(separator: "_")
- }
- var completionFunctionPrefix: String {
- "__\(initialCommand)"
- }
- }
- extension ArgumentInfoV0 {
- /// Returns a string with the arguments for the callback to generate custom
- /// completions for this argument.
- func commonCustomCompletionCall(command: CommandInfoV0) -> String {
- let subcommandNames =
- command.commandContext.dropFirst().map { "\($0) " }.joined()
- let argumentName: String
- switch kind {
- case .positional:
- if let index = command.positionalArguments.firstIndex(of: self) {
- argumentName = "positional@\(index)"
- } else {
- argumentName = "---"
- }
- default:
- argumentName = preferredName?.commonCompletionSynopsisString() ?? "---"
- }
- return "---completion \(subcommandNames)-- \(argumentName)"
- }
- }
- extension ArgumentInfoV0.NameInfoV0 {
- func commonCompletionSynopsisString() -> String {
- switch kind {
- case .long:
- return "--\(name)"
- case .short, .longWithSingleDash:
- return "-\(name)"
- }
- }
- }
|