Skip to content

Commit 392eb16

Browse files
committed
Argument Description for auto-generated --help
1 parent 1da2194 commit 392eb16

File tree

8 files changed

+332
-19
lines changed

8 files changed

+332
-19
lines changed

Commander.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
2720BDED1BB348BC00C09984 /* ArgumentDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2720BDEC1BB348BC00C09984 /* ArgumentDescription.swift */; settings = {ASSET_TAGS = (); }; };
1011
2768A2241BACC38C00F994EE /* Commander.h in Headers */ = {isa = PBXBuildFile; fileRef = 2768A2231BACC38C00F994EE /* Commander.h */; settings = {ATTRIBUTES = (Public, ); }; };
1112
2768A22B1BACC38C00F994EE /* Commander.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2768A2201BACC38C00F994EE /* Commander.framework */; settings = {ASSET_TAGS = (); }; };
1213
2768A23B1BACC3B600F994EE /* ArgumentParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2768A23A1BACC3B600F994EE /* ArgumentParser.swift */; settings = {ASSET_TAGS = (); }; };
@@ -33,6 +34,7 @@
3334
/* End PBXContainerItemProxy section */
3435

3536
/* Begin PBXFileReference section */
37+
2720BDEC1BB348BC00C09984 /* ArgumentDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgumentDescription.swift; sourceTree = "<group>"; };
3638
27593EE51BB335590018278E /* UniversalFramework_Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UniversalFramework_Base.xcconfig; sourceTree = "<group>"; };
3739
27593EE61BB335590018278E /* UniversalFramework_Framework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UniversalFramework_Framework.xcconfig; sourceTree = "<group>"; };
3840
27593EE71BB335590018278E /* UniversalFramework_Test.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UniversalFramework_Test.xcconfig; sourceTree = "<group>"; };
@@ -110,6 +112,7 @@
110112
2768A2231BACC38C00F994EE /* Commander.h */,
111113
2768A23A1BACC3B600F994EE /* ArgumentParser.swift */,
112114
279256B51BB32A8E00E66B9E /* ArgumentConvertible.swift */,
115+
2720BDEC1BB348BC00C09984 /* ArgumentDescription.swift */,
113116
279256A91BB3226E00E66B9E /* CommandType.swift */,
114117
279256AB1BB322FA00E66B9E /* Command.swift */,
115118
279256B31BB3260D00E66B9E /* Group.swift */,
@@ -241,6 +244,7 @@
241244
buildActionMask = 2147483647;
242245
files = (
243246
2768A23B1BACC3B600F994EE /* ArgumentParser.swift in Sources */,
247+
2720BDED1BB348BC00C09984 /* ArgumentDescription.swift in Sources */,
244248
279256AA1BB3226E00E66B9E /* CommandType.swift in Sources */,
245249
279256AC1BB322FA00E66B9E /* Command.swift in Sources */,
246250
279256B61BB32A8E00E66B9E /* ArgumentConvertible.swift in Sources */,
Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
public enum ArgumentError : ErrorType, CustomStringConvertible {
2-
case MissingValue(String?)
2+
case MissingValue(argument: String?)
33

44
/// Value is not convertible to type
5-
case InvalidType(value:String, type:String)
5+
case InvalidType(value:String, type:String, argument:String?)
66

77
public var description:String {
88
switch self {
99
case .MissingValue:
1010
return "Missing argument"
11-
case .InvalidType(let value, let type):
12-
return "\(value) is not a \(type)"
11+
case .InvalidType(let value, let type, let argument):
12+
if let argument = argument {
13+
return "`\(value)` is not a valid `\(type)` for `\(argument)`"
14+
}
15+
return "`\(value)` is not a `\(type)`"
1316
}
1417
}
1518
}
1619

17-
public protocol ArgumentConvertible {
20+
public protocol ArgumentConvertible : CustomStringConvertible {
1821
/// Initialise the type with an ArgumentParser
1922
init(parser: ArgumentParser) throws
2023
}
2124

22-
extension String : ArgumentConvertible {
25+
extension String : ArgumentConvertible, CustomStringConvertible {
2326
public init(parser: ArgumentParser) throws {
2427
if let value = parser.shift() {
2528
self.init(value)
2629
} else {
27-
throw ArgumentError.MissingValue(nil)
30+
throw ArgumentError.MissingValue(argument: nil)
2831
}
2932
}
33+
34+
public var description:String {
35+
return self
36+
}
3037
}
3138

3239
extension Int : ArgumentConvertible {
@@ -35,10 +42,10 @@ extension Int : ArgumentConvertible {
3542
if let value = Int(value) {
3643
self.init(value)
3744
} else {
38-
throw ArgumentError.InvalidType(value: value, type: "number")
45+
throw ArgumentError.InvalidType(value: value, type: "number", argument: nil)
3946
}
4047
} else {
41-
throw ArgumentError.MissingValue(nil)
48+
throw ArgumentError.MissingValue(argument: nil)
4249
}
4350
}
4451
}
@@ -50,10 +57,10 @@ extension Float : ArgumentConvertible {
5057
if let value = Float(value) {
5158
self.init(value)
5259
} else {
53-
throw ArgumentError.InvalidType(value: value, type: "number")
60+
throw ArgumentError.InvalidType(value: value, type: "number", argument: nil)
5461
}
5562
} else {
56-
throw ArgumentError.MissingValue(nil)
63+
throw ArgumentError.MissingValue(argument: nil)
5764
}
5865
}
5966
}
@@ -65,10 +72,10 @@ extension Double : ArgumentConvertible {
6572
if let value = Double(value) {
6673
self.init(value)
6774
} else {
68-
throw ArgumentError.InvalidType(value: value, type: "number")
75+
throw ArgumentError.InvalidType(value: value, type: "number", argument: nil)
6976
}
7077
} else {
71-
throw ArgumentError.MissingValue(nil)
78+
throw ArgumentError.MissingValue(argument: nil)
7279
}
7380
}
7481
}
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
public enum ArgumentType {
2+
case Argument
3+
case Option
4+
}
5+
6+
public protocol ArgumentDescriptor {
7+
typealias ValueType
8+
9+
/// The arguments name
10+
var name:String { get }
11+
12+
/// The arguments description
13+
var description:String? { get }
14+
15+
var type:ArgumentType { get }
16+
17+
/// Parse the argument
18+
func parse(parser:ArgumentParser) throws -> ValueType
19+
}
20+
21+
extension ArgumentConvertible {
22+
init(string: String) throws {
23+
try self.init(parser: ArgumentParser(arguments: [string]))
24+
}
25+
}
26+
27+
public class Argument<T : ArgumentConvertible> : ArgumentDescriptor {
28+
public typealias ValueType = T
29+
30+
public let name:String
31+
public let description:String?
32+
33+
public var type:ArgumentType { return .Argument }
34+
35+
public init(_ name:String, description:String? = nil) {
36+
self.name = name
37+
self.description = description
38+
}
39+
40+
public func parse(parser:ArgumentParser) throws -> ValueType {
41+
return try T(parser: parser)
42+
}
43+
}
44+
45+
46+
public class Option<T : ArgumentConvertible> : ArgumentDescriptor {
47+
public typealias ValueType = T
48+
49+
public let name:String
50+
public let description:String?
51+
public let `default`:ValueType
52+
public var type:ArgumentType { return .Option }
53+
54+
public init(_ name:String, _ `default`:ValueType, description:String? = nil) {
55+
self.name = name
56+
self.description = description
57+
self.`default` = `default`
58+
}
59+
60+
public func parse(parser:ArgumentParser) throws -> ValueType {
61+
if let value = try parser.shiftValueForOption(name) {
62+
return try T(string: value)
63+
}
64+
65+
return `default`
66+
}
67+
}
68+
69+
public class Options<T : ArgumentConvertible> : ArgumentDescriptor {
70+
public typealias ValueType = [T]
71+
72+
public let name:String
73+
public let description:String?
74+
public let count:Int
75+
public let `default`:ValueType
76+
public var type:ArgumentType { return .Option }
77+
78+
public init(_ name:String, _ `default`:ValueType, count: Int, description:String? = nil) {
79+
self.name = name
80+
self.`default` = `default`
81+
self.count = count
82+
self.description = description
83+
}
84+
85+
public func parse(parser:ArgumentParser) throws -> ValueType {
86+
let values = try parser.shiftValuesForOption(name, count: count)
87+
return try values?.map { try T(string: $0) } ?? `default`
88+
}
89+
}
90+
91+
public class Flag : ArgumentDescriptor {
92+
public typealias ValueType = Bool
93+
94+
public let name:String
95+
public let flag:Character?
96+
public let description:String?
97+
public let `default`:ValueType
98+
public var type:ArgumentType { return .Option }
99+
100+
public init(_ name:String, flag:Character? = nil, description:String? = nil, `default`:Bool = false) {
101+
self.name = name
102+
self.flag = flag
103+
self.description = description
104+
self.`default` = `default`
105+
}
106+
107+
public func parse(parser:ArgumentParser) throws -> ValueType {
108+
if parser.hasOption("no-\(name)") {
109+
return false
110+
}
111+
112+
if parser.hasOption(name) {
113+
return true
114+
}
115+
116+
if let flag = flag {
117+
if parser.hasFlag(flag) {
118+
return true
119+
}
120+
}
121+
122+
return `default`
123+
}
124+
}
125+
126+
class BoxedArgumentDescriptor {
127+
let name:String
128+
let description:String?
129+
let `default`:String?
130+
let type:ArgumentType
131+
132+
init<T : ArgumentDescriptor>(value:T) {
133+
name = value.name
134+
description = value.description
135+
type = value.type
136+
137+
if let value = value as? Flag {
138+
`default` = value.`default`.description
139+
} else {
140+
// TODO, default for Option and Options
141+
`default` = nil
142+
}
143+
}
144+
}
145+
146+
class Help : ErrorType, CustomStringConvertible {
147+
let command:String?
148+
let group:Group?
149+
let descriptors:[BoxedArgumentDescriptor]
150+
151+
init(_ descriptors:[BoxedArgumentDescriptor], command:String? = nil, group:Group? = nil) {
152+
self.command = command
153+
self.group = group
154+
self.descriptors = descriptors
155+
}
156+
157+
func reraise(command:String? = nil) -> Help {
158+
if let oldCommand = self.command, newCommand = command {
159+
return Help(descriptors, command: "\(newCommand) \(oldCommand)")
160+
}
161+
return Help(descriptors, command: command ?? self.command)
162+
}
163+
164+
var description:String {
165+
var output = [String]()
166+
167+
let arguments = descriptors.filter { $0.type == ArgumentType.Argument }
168+
let options = descriptors.filter { $0.type == ArgumentType.Option }
169+
170+
if let command = command {
171+
let args = arguments.map { $0.name }
172+
let usage = ([command] + args).joinWithSeparator(" ")
173+
174+
output.append("Usage:")
175+
output.append("")
176+
output.append(" \(usage)")
177+
output.append("")
178+
}
179+
180+
if let group = group {
181+
output.append("Commands:")
182+
output.append("")
183+
for (name, _) in group.commands {
184+
output.append(" + \(name)")
185+
}
186+
output.append("")
187+
}
188+
189+
if !options.isEmpty {
190+
output.append("Options:")
191+
for option in options {
192+
// TODO: default, [default: `\(`default`)`]
193+
194+
if let description = option.description {
195+
output.append(" --\(option.name) - \(description)")
196+
} else {
197+
output.append(" --\(option.name)")
198+
}
199+
}
200+
}
201+
202+
return output.joinWithSeparator("\n")
203+
}
204+
}
205+
206+
207+
public func command<A : ArgumentDescriptor>(descriptor:A, closure:((A.ValueType) -> ())) -> CommandType {
208+
return AnonymousCommand { parser in
209+
if parser.hasOption("help") {
210+
throw Help([BoxedArgumentDescriptor(value: descriptor)])
211+
}
212+
213+
closure(try descriptor.parse(parser))
214+
}
215+
}
216+
217+
public func command<A:ArgumentDescriptor, B:ArgumentDescriptor>(descriptorA:A, _ descriptorB:B, closure:((A.ValueType, B.ValueType) -> ())) -> CommandType {
218+
return AnonymousCommand { parser in
219+
if parser.hasOption("help") {
220+
throw Help([
221+
BoxedArgumentDescriptor(value: descriptorA),
222+
BoxedArgumentDescriptor(value: descriptorB),
223+
])
224+
}
225+
226+
closure(try descriptorA.parse(parser), try descriptorB.parse(parser))
227+
}
228+
}
229+
230+
public func command<A:ArgumentDescriptor, B:ArgumentDescriptor, C:ArgumentDescriptor>(descriptorA:A, _ descriptorB:B, _ descriptorC:C, closure:((A.ValueType, B.ValueType, C.ValueType) -> ())) -> CommandType {
231+
return AnonymousCommand { parser in
232+
if parser.hasOption("help") {
233+
throw Help([
234+
BoxedArgumentDescriptor(value: descriptorA),
235+
BoxedArgumentDescriptor(value: descriptorB),
236+
BoxedArgumentDescriptor(value: descriptorC),
237+
])
238+
}
239+
240+
closure(try descriptorA.parse(parser), try descriptorB.parse(parser), try descriptorC.parse(parser))
241+
}
242+
}

Commander/ArgumentParser.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public struct ArgumentParserError : ErrorType, CustomStringConvertible {
4141
}
4242

4343

44-
public final class ArgumentParser : ArgumentConvertible {
44+
public final class ArgumentParser : ArgumentConvertible, CustomStringConvertible {
4545
private var arguments:[Arg]
4646

4747
/// Initialises the ArgumentParser with an array of arguments
@@ -66,6 +66,10 @@ public final class ArgumentParser : ArgumentConvertible {
6666
arguments = parser.arguments
6767
}
6868

69+
public var description:String {
70+
return ""
71+
}
72+
6973
/// Returns the first positional argument in the remaining arguments.
7074
/// This will remove the argument from the remaining arguments.
7175
public func shift() -> String? {

0 commit comments

Comments
 (0)