File: ParsableCommand.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (262 lines) | stat: -rw-r--r-- 9,759 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//===----------------------------------------------------------*- swift -*-===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// A type that can be executed as part of a nested tree of commands.
public protocol ParsableCommand: ParsableArguments {
  /// Configuration for this command, including subcommands and custom help
  /// text.
  static var configuration: CommandConfiguration { get }
  
  /// *For internal use only:* The name for the command on the command line.
  ///
  /// This is generated from the configuration, if given, or from the type
  /// name if not. This is a customization point so that a WrappedParsable
  /// can pass through the wrapped type's name.
  static var _commandName: String { get }
  
  /// The behavior or functionality of this command.
  ///
  /// Implement this method in your `ParsableCommand`-conforming type with the
  /// functionality that this command represents.
  ///
  /// This method has a default implementation that prints the help screen for
  /// this command.
  mutating func run() throws
}

// MARK: - Default implementations

extension ParsableCommand {
  public static var _commandName: String {
    configuration.commandName ??
      String(describing: Self.self).convertedToSnakeCase(separator: "-")
  }
  
  public static var configuration: CommandConfiguration {
    CommandConfiguration()
  }

  public mutating func run() throws {
    throw CleanExit.helpRequest(self)
  }
}

// MARK: - API

extension ParsableCommand {
  /// Parses an instance of this type, or one of its subcommands, from
  /// command-line arguments.
  ///
  /// - Parameter arguments: An array of arguments to use for parsing. If
  ///   `arguments` is `nil`, this uses the program's command-line arguments.
  /// - Returns: A new instance of this type, one of its subcommands, or a
  ///   command type internal to the `ArgumentParser` library.
  public static func parseAsRoot(
    _ arguments: [String]? = nil
  ) throws -> ParsableCommand {
    var parser = CommandParser(self)
    let arguments = arguments ?? Array(CommandLine.arguments.dropFirst())
    return try parser.parse(arguments: arguments).get()
  }
  
  /// Returns the text of the help screen for the given subcommand of this
  /// command.
  ///
  /// - Parameters:
  ///   - subcommand: The subcommand to generate the help screen for.
  ///     `subcommand` must be declared in the subcommand tree of this
  ///     command.
  ///   - columns: The column width to use when wrapping long line in the
  ///     help screen. If `columns` is `nil`, uses the current terminal
  ///     width, or a default value of `80` if the terminal width is not
  ///     available.
  /// - Returns: The full help screen for this type.
  @_disfavoredOverload
  @available(*, deprecated, renamed: "helpMessage(for:includeHidden:columns:)")
  public static func helpMessage(
    for _subcommand: ParsableCommand.Type,
    columns: Int? = nil
  ) -> String {
    helpMessage(for: _subcommand, includeHidden: false, columns: columns)
  }

  /// Returns the text of the help screen for the given subcommand of this
  /// command.
  ///
  /// - Parameters:
  ///   - subcommand: The subcommand to generate the help screen for.
  ///     `subcommand` must be declared in the subcommand tree of this
  ///     command.
  ///   - includeHidden: Include hidden help information in the generated
  ///     message.
  ///   - columns: The column width to use when wrapping long line in the
  ///     help screen. If `columns` is `nil`, uses the current terminal
  ///     width, or a default value of `80` if the terminal width is not
  ///     available.
  /// - Returns: The full help screen for this type.
  public static func helpMessage(
    for subcommand: ParsableCommand.Type,
    includeHidden: Bool = false,
    columns: Int? = nil
  ) -> String {
    HelpGenerator(
      commandStack: CommandParser(self).commandStack(for: subcommand),
      visibility: includeHidden ? .hidden : .default)
        .rendered(screenWidth: columns)
  }

  /// Executes this command, or one of its subcommands, with the given
  /// arguments.
  ///
  /// This method parses an instance of this type, one of its subcommands, or
  /// another built-in `ParsableCommand` type, from command-line arguments,
  /// and then calls its `run()` method, exiting with a relevant error message
  /// if necessary.
  ///
  /// - Parameter arguments: An array of arguments to use for parsing. If
  ///   `arguments` is `nil`, this uses the program's command-line arguments.
  public static func main(_ arguments: [String]?) {
    
#if DEBUG
    if #available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *) {
      if let asyncCommand = firstAsyncSubcommand(self) {
        if Self() is AsyncParsableCommand {
          failAsyncPlatform(rootCommand: self)
        } else {
          failAsyncHierarchy(rootCommand: self, subCommand: asyncCommand)
        }
      }
    }
#endif
    
    do {
      var command = try parseAsRoot(arguments)
      try command.run()
    } catch {
      exit(withError: error)
    }
  }

  /// Executes this command, or one of its subcommands, with the program's
  /// command-line arguments.
  ///
  /// Instead of calling this method directly, you can add `@main` to the root
  /// command for your command-line tool.
  ///
  /// This method parses an instance of this type, one of its subcommands, or
  /// another built-in `ParsableCommand` type, from command-line arguments,
  /// and then calls its `run()` method, exiting with a relevant error message
  /// if necessary.
  public static func main() {
    self.main(nil)
  }
}

// MARK: - Internal API

extension ParsableCommand {
  /// `true` if this command contains any array arguments that are declared
  /// with `.unconditionalRemaining`.
  internal static var includesPassthroughArguments: Bool {
    ArgumentSet(self, visibility: .private, parent: nil).contains(where: {
      $0.isRepeatingPositional && $0.parsingStrategy == .allRemainingInput
    })
  }
  
  internal static var includesAllUnrecognizedArgument: Bool {
    ArgumentSet(self, visibility: .private, parent: nil).contains(where: {
      $0.isRepeatingPositional && $0.parsingStrategy == .allUnrecognized
    })
  }
  
  /// `true` if this command's default subcommand contains any array arguments
  /// that are declared with `.unconditionalRemaining`. This is `false` if
  /// there's no default subcommand.
  internal static var defaultIncludesPassthroughArguments: Bool {
    configuration.defaultSubcommand?.includesPassthroughArguments == true
  }
  
#if DEBUG
  @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
  internal static func checkAsyncHierarchy(_ command: ParsableCommand.Type, root: String) {
    for sub in command.configuration.subcommands {
      checkAsyncHierarchy(sub, root: root)

      guard sub.configuration.subcommands.isEmpty else { continue }
      guard sub is AsyncParsableCommand.Type else { continue }

      fatalError("""

      --------------------------------------------------------------------
      Asynchronous subcommand of a synchronous root.

      The asynchronous command `\(sub)` is declared as a subcommand of the synchronous root command `\(root)`.

      With this configuration, your asynchronous `run()` method will not be called. To fix this issue, change `\(root)`'s `ParsableCommand` conformance to `AsyncParsableCommand`.
      --------------------------------------------------------------------

      """.wrapped(to: 70))
    }
  }

  @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
  internal static func firstAsyncSubcommand(_ command: ParsableCommand.Type) -> AsyncParsableCommand.Type? {
    for sub in command.configuration.subcommands {
      if let asyncCommand = sub as? AsyncParsableCommand.Type,
         sub.configuration.subcommands.isEmpty
      {
        return asyncCommand
      }
      
      if let asyncCommand = firstAsyncSubcommand(sub) {
        return asyncCommand
      }
    }
    
    return nil
  }
#endif
}

// MARK: Async Configuration Errors

func failAsyncHierarchy(
  rootCommand: ParsableCommand.Type, subCommand: ParsableCommand.Type
) -> Never {
  fatalError("""

  --------------------------------------------------------------------
  Asynchronous subcommand of a synchronous root.

  The asynchronous command `\(subCommand)` is declared as a subcommand of the synchronous root command `\(rootCommand)`.

  With this configuration, your asynchronous `run()` method will not be called. To fix this issue, change `\(rootCommand)`'s `ParsableCommand` conformance to `AsyncParsableCommand`.
  --------------------------------------------------------------------

  """.wrapped(to: 70))
}

func failAsyncPlatform(rootCommand: ParsableCommand.Type) -> Never {
  fatalError("""

  --------------------------------------------------------------------
  Asynchronous root command needs availability annotation.

  The asynchronous root command `\(rootCommand)` needs an availability annotation in order to be executed asynchronously. To fix this issue, add the following availability attribute to your `\(rootCommand)` declaration or set the minimum platform in your "Package.swift" file.
  
  """.wrapped(to: 70)
  + """
  
  @available(macOS 10.15, macCatalyst 13, iOS 13, tvOS 13, watchOS 6, *)
  --------------------------------------------------------------------
  
  """)
}