-
Notifications
You must be signed in to change notification settings - Fork 4
/
Commandant.swift
453 lines (387 loc) · 18.8 KB
/
Commandant.swift
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
import Foundation
import SwiftOnoneSupport
/// Applies `f` to the value in the given result.
///
/// In the context of command-line option parsing, this is used to chain
/// together the parsing of multiple arguments. See OptionsProtocol for an example.
public func <*> <T, U, ClientError>(f: (T) -> U, value: Result<T, Commandant.CommandantError<ClientError>>) -> Result<U, Commandant.CommandantError<ClientError>>
/// Applies the function in `f` to the value in the given result.
///
/// In the context of command-line option parsing, this is used to chain
/// together the parsing of multiple arguments. See OptionsProtocol for an example.
public func <*> <T, U, ClientError>(f: Result<(T) -> U, Commandant.CommandantError<ClientError>>, value: Result<T, Commandant.CommandantError<ClientError>>) -> Result<U, Commandant.CommandantError<ClientError>>
/// Describes an argument that can be provided on the command line.
public struct Argument<T> {
/// The default value for this argument. This is the value that will be used
/// if the argument is never explicitly specified on the command line.
///
/// If this is nil, this argument is always required.
public let defaultValue: T?
/// A human-readable string describing the purpose of this argument. This will
/// be shown in help messages.
public let usage: String
/// A human-readable string that describes this argument as a paramater shown
/// in the list of possible parameters in help messages (e.g. for "paths", the
/// user would see <paths…>).
public let usageParameter: String?
public init(defaultValue: T? = nil, usage: String, usageParameter: String? = nil)
}
/// Destructively parses a list of command-line arguments.
public final class ArgumentParser {
/// Initializes the generator from a simple list of command-line arguments.
public init(_ arguments: [String])
}
/// Represents a value that can be converted from a command-line argument.
public protocol ArgumentProtocol {
/// A human-readable name for this type.
static var name: String { get }
/// Attempts to parse a value from the given command-line argument.
static func from(string: String) -> Self?
}
/// Describes the "mode" in which a command should run.
public enum CommandMode {
/// Options should be parsed from the given command-line arguments.
case arguments(Commandant.ArgumentParser)
/// Each option should record its usage information in an error, for
/// presentation to the user.
case usage
}
extension CommandMode {
/// Evaluates the given argument in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, the argument's `defaultValue` is used.
public static func <| <T, ClientError>(mode: Commandant.CommandMode, argument: Commandant.Argument<T>) -> Result<T, Commandant.CommandantError<ClientError>> where T: Commandant.ArgumentProtocol
/// Evaluates the given argument list in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, the argument's `defaultValue` is used.
public static func <| <T, ClientError>(mode: Commandant.CommandMode, argument: Commandant.Argument<[T]>) -> Result<[T], Commandant.CommandantError<ClientError>> where T: Commandant.ArgumentProtocol
}
extension CommandMode {
/// Evaluates the given option in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, the option's `defaultValue` is used.
public static func <| <T, ClientError>(mode: Commandant.CommandMode, option: Commandant.Option<T>) -> Result<T, Commandant.CommandantError<ClientError>> where T: Commandant.ArgumentProtocol
/// Evaluates the given option in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, `nil` is used.
public static func <| <T, ClientError>(mode: Commandant.CommandMode, option: Commandant.Option<T?>) -> Result<T?, Commandant.CommandantError<ClientError>> where T: Commandant.ArgumentProtocol
/// Evaluates the given option in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, the option's `defaultValue` is used.
public static func <| <T, ClientError>(mode: Commandant.CommandMode, option: Commandant.Option<[T]>) -> Result<[T], Commandant.CommandantError<ClientError>> where T: Commandant.ArgumentProtocol
/// Evaluates the given option in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, `nil` is used.
public static func <| <T, ClientError>(mode: Commandant.CommandMode, option: Commandant.Option<[T]?>) -> Result<[T]?, Commandant.CommandantError<ClientError>> where T: Commandant.ArgumentProtocol
/// Evaluates the given boolean option in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, the option's `defaultValue` is used.
public static func <| <ClientError>(mode: Commandant.CommandMode, option: Commandant.Option<Bool>) -> Result<Bool, Commandant.CommandantError<ClientError>>
}
extension CommandMode {
/// Evaluates the given boolean switch in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, the option's `defaultValue` is used.
public static func <| <ClientError>(mode: Commandant.CommandMode, option: Commandant.Switch) -> Result<Bool, Commandant.CommandantError<ClientError>>
}
/// Represents a subcommand that can be executed with its own set of arguments.
public protocol CommandProtocol {
/// The command's options type.
associatedtype Options: Commandant.OptionsProtocol
associatedtype ClientError where Self.ClientError == Self.Options.ClientError
/// The action that users should specify to use this subcommand (e.g.,
/// `help`).
var verb: String { get }
/// A human-readable, high-level description of what this command is used
/// for.
var function: String { get }
/// Runs this subcommand with the given options.
func run(_ options: Self.Options) -> Result<Void, Self.ClientError>
}
/// Maintains the list of commands available to run.
public final class CommandRegistry<ClientError> where ClientError: Error {
/// All available commands.
public var commands: [Commandant.CommandWrapper<ClientError>] { get }
public init()
/// Registers the given commands, making those available to run.
///
/// If another commands were already registered with the same `verb`s, those
/// will be overwritten.
public func register<C>(_ commands: C...) -> Commandant.CommandRegistry<ClientError> where ClientError == C.ClientError, C: Commandant.CommandProtocol
/// Runs the command corresponding to the given verb, passing it the given
/// arguments.
///
/// Returns the results of the execution, or nil if no such command exists.
public func run(command verb: String, arguments: [String]) -> Result<Void, Commandant.CommandantError<ClientError>>?
/// Returns the command matching the given verb, or nil if no such command
/// is registered.
public subscript(_: String) -> Commandant.CommandWrapper<ClientError>? { get }
}
extension CommandRegistry {
/// Hands off execution to the CommandRegistry, by parsing CommandLine.arguments
/// and then running whichever command has been identified in the argument
/// list.
///
/// If the chosen command executes successfully, the process will exit with
/// a successful exit code.
///
/// If the chosen command fails, the provided error handler will be invoked,
/// then the process will exit with a failure exit code.
///
/// If a matching command could not be found but there is any `executable-verb`
/// style subcommand executable in the caller's `$PATH`, the subcommand will
/// be executed.
///
/// If a matching command could not be found or a usage error occurred,
/// a helpful error message will be written to `stderr`, then the process
/// will exit with a failure error code.
public func main(defaultVerb: String, errorHandler: (ClientError) -> Void) -> Never
/// Hands off execution to the CommandRegistry, by parsing `arguments`
/// and then running whichever command has been identified in the argument
/// list.
///
/// If the chosen command executes successfully, the process will exit with
/// a successful exit code.
///
/// If the chosen command fails, the provided error handler will be invoked,
/// then the process will exit with a failure exit code.
///
/// If a matching command could not be found but there is any `executable-verb`
/// style subcommand executable in the caller's `$PATH`, the subcommand will
/// be executed.
///
/// If a matching command could not be found or a usage error occurred,
/// a helpful error message will be written to `stderr`, then the process
/// will exit with a failure error code.
public func main(arguments: [String], defaultVerb: String, errorHandler: (ClientError) -> Void) -> Never
}
/// A type-erased command.
public struct CommandWrapper<ClientError> where ClientError: Error {
public let verb: String
public let function: String
public let run: (Commandant.ArgumentParser) -> Result<Void, Commandant.CommandantError<ClientError>>
public let usage: () -> Commandant.CommandantError<ClientError>?
}
/// Possible errors that can originate from Commandant.
///
/// `ClientError` should be the type of error (if any) that can occur when
/// running commands.
public enum CommandantError<ClientError>: Error {
/// An option was used incorrectly.
case usageError(description: String)
/// An error occurred while running a command.
case commandError(ClientError)
}
extension CommandantError: CustomStringConvertible {
/// A textual representation of this instance.
///
/// Calling this property directly is discouraged. Instead, convert an
/// instance of any type to a string by using the `String(describing:)`
/// initializer. This initializer works with any type, and uses the custom
/// `description` property for types that conform to
/// `CustomStringConvertible`:
///
/// struct Point: CustomStringConvertible {
/// let x: Int, y: Int
///
/// var description: String {
/// return "(\(x), \(y))"
/// }
/// }
///
/// let p = Point(x: 21, y: 30)
/// let s = String(describing: p)
/// print(s)
/// // Prints "(21, 30)"
///
/// The conversion of `p` to a string in the assignment to `s` uses the
/// `Point` type's `description` property.
public var description: String { get }
}
/// A basic implementation of a `help` command, using information available in a
/// `CommandRegistry`.
///
/// If you want to use this command, initialize it with the registry, then add
/// it to that same registry:
///
/// let commands: CommandRegistry<MyErrorType> = …
/// let helpCommand = HelpCommand(registry: commands)
/// commands.register(helpCommand)
public struct HelpCommand<ClientError>: Commandant.CommandProtocol where ClientError: Error {
/// The command's options type.
public typealias Options = Commandant.HelpOptions<ClientError>
/// The action that users should specify to use this subcommand (e.g.,
/// `help`).
public let verb: String
/// A human-readable, high-level description of what this command is used
/// for.
public let function: String
/// Initializes the command to provide help from the given registry of
/// commands.
public init(registry: Commandant.CommandRegistry<ClientError>, function: String? = nil)
/// Runs this subcommand with the given options.
public func run(_ options: Commandant.HelpCommand<ClientError>.Options) -> Result<Void, ClientError>
}
public struct HelpOptions<ClientError>: Commandant.OptionsProtocol where ClientError: Error {
/// Evaluates this set of options in the given mode.
///
/// Returns the parsed options or a `UsageError`.
public static func evaluate(_ m: Commandant.CommandMode) -> Result<Commandant.HelpOptions<ClientError>, Commandant.CommandantError<ClientError>>
}
/// An `OptionsProtocol` that has no options.
public struct NoOptions<ClientError>: Commandant.OptionsProtocol where ClientError: Error {
public init()
/// Evaluates this set of options in the given mode.
///
/// Returns the parsed options or a `UsageError`.
public static func evaluate(_ m: Commandant.CommandMode) -> Result<Commandant.NoOptions<ClientError>, Commandant.CommandantError<ClientError>>
}
/// Describes an option that can be provided on the command line.
public struct Option<T> {
/// The key that controls this option. For example, a key of `verbose` would
/// be used for a `--verbose` option.
public let key: String
/// The default value for this option. This is the value that will be used
/// if the option is never explicitly specified on the command line.
public let defaultValue: T
/// A human-readable string describing the purpose of this option. This will
/// be shown in help messages.
///
/// For boolean operations, this should describe the effect of _not_ using
/// the default value (i.e., what will happen if you disable/enable the flag
/// differently from the default).
public let usage: String
public init(key: String, defaultValue: T, usage: String)
}
extension Option: CustomStringConvertible {
/// A textual representation of this instance.
///
/// Calling this property directly is discouraged. Instead, convert an
/// instance of any type to a string by using the `String(describing:)`
/// initializer. This initializer works with any type, and uses the custom
/// `description` property for types that conform to
/// `CustomStringConvertible`:
///
/// struct Point: CustomStringConvertible {
/// let x: Int, y: Int
///
/// var description: String {
/// return "(\(x), \(y))"
/// }
/// }
///
/// let p = Point(x: 21, y: 30)
/// let s = String(describing: p)
/// print(s)
/// // Prints "(21, 30)"
///
/// The conversion of `p` to a string in the assignment to `s` uses the
/// `Point` type's `description` property.
public var description: String { get }
}
/// Represents a record of options for a command, which can be parsed from
/// a list of command-line arguments.
///
/// This is most helpful when used in conjunction with the `Option` and `Switch`
/// types, and `<*>` and `<|` combinators.
///
/// Example:
///
/// struct LogOptions: OptionsProtocol {
/// let verbosity: Int
/// let outputFilename: String
/// let shouldDelete: Bool
/// let logName: String
///
/// static func create(_ verbosity: Int) -> (String) -> (Bool) -> (String) -> LogOptions {
/// return { outputFilename in { shouldDelete in { logName in LogOptions(verbosity: verbosity, outputFilename: outputFilename, shouldDelete: shouldDelete, logName: logName) } } }
/// }
///
/// static func evaluate(_ m: CommandMode) -> Result<LogOptions, CommandantError<YourErrorType>> {
/// return create
/// <*> m <| Option(key: "verbose", defaultValue: 0, usage: "the verbosity level with which to read the logs")
/// <*> m <| Option(key: "outputFilename", defaultValue: "", usage: "a file to print output to, instead of stdout")
/// <*> m <| Switch(flag: "d", key: "delete", usage: "delete the logs when finished")
/// <*> m <| Argument(usage: "the log to read")
/// }
/// }
public protocol OptionsProtocol {
associatedtype ClientError: Error
/// Evaluates this set of options in the given mode.
///
/// Returns the parsed options or a `UsageError`.
static func evaluate(_ m: Commandant.CommandMode) -> Result<Self, Commandant.CommandantError<Self.ClientError>>
}
/// Describes a parameterless command line flag that defaults to false and can only
/// be switched on. Canonical examples include `--force` and `--recurse`.
///
/// For a boolean toggle that can be enabled and disabled use `Option<Bool>`.
public struct Switch {
/// The key that enables this switch. For example, a key of `verbose` would be
/// used for a `--verbose` option.
public let key: String
/// Optional single letter flag that enables this switch. For example, `-v` would
/// be used as a shorthand for `--verbose`.
///
/// Multiple flags can be grouped together as a single argument and will split
/// when parsing (e.g. `rm -rf` treats 'r' and 'f' as inidividual flags).
public let flag: Character?
/// A human-readable string describing the purpose of this option. This will
/// be shown in help messages.
public let usage: String
public init(flag: Character? = nil, key: String, usage: String)
}
extension Switch: CustomStringConvertible {
/// A textual representation of this instance.
///
/// Calling this property directly is discouraged. Instead, convert an
/// instance of any type to a string by using the `String(describing:)`
/// initializer. This initializer works with any type, and uses the custom
/// `description` property for types that conform to
/// `CustomStringConvertible`:
///
/// struct Point: CustomStringConvertible {
/// let x: Int, y: Int
///
/// var description: String {
/// return "(\(x), \(y))"
/// }
/// }
///
/// let p = Point(x: 21, y: 30)
/// let s = String(describing: p)
/// print(s)
/// // Prints "(21, 30)"
///
/// The conversion of `p` to a string in the assignment to `s` uses the
/// `Point` type's `description` property.
public var description: String { get }
}
extension Int: Commandant.ArgumentProtocol {
/// A human-readable name for this type.
public static let name: String
/// Attempts to parse a value from the given command-line argument.
public static func from(string: String) -> Int?
}
extension String: Commandant.ArgumentProtocol {
/// A human-readable name for this type.
public static let name: String
/// Attempts to parse a value from the given command-line argument.
public static func from(string: String) -> String?
}
extension RawRepresentable where Self: Commandant.ArgumentProtocol, Self.RawValue: StringProtocol {
public static func from(string: String) -> Self?
}
extension RawRepresentable where Self: Commandant.ArgumentProtocol, Self.RawValue: FixedWidthInteger {
public static func from(string: String) -> Self?
}
infix operator <*>: LogicalDisjunctionPrecedence
infix operator <|: MultiplicationPrecedence