Skip to content

Commit 11e9242

Browse files
authored
Command generated help (#8)
* made init of Command public. * updated for swift 4.2 * added discardable result. * moved createTable to Help class. * fixed config to runt with Swift 4.0 as well. * updated xcode build image for travis. * added automated help for commands and support for multiline help texts. * fixed lint issue
1 parent add50ec commit 11e9242

File tree

6 files changed

+219
-40
lines changed

6 files changed

+219
-40
lines changed

Sources/ArgTree.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ public class ArgTree: ParserNode {
110110
let printHelp: () -> Void = {
111111
[unowned self] in
112112
var rows: [[String]] = []
113-
self.parsers
114-
.flatMap({ $0.description })
113+
self.flatMap({ $0.description })
115114
.forEach({ (argument: String, description: String) in
116115
rows.append([" ", argument, description])
117116
})

Sources/parsers/Command.swift

+41
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
#if os(macOS)
2+
import Darwin
3+
#elseif os(Linux)
4+
import Glibc
5+
#endif
6+
import LoggerAPI
7+
18
public enum CommandParseError: Error {
29
case commandAllowedOnlyOnce(command: Command, atIndex: Int)
310
}
@@ -9,17 +16,34 @@ open class Command: ValueParser<Bool>, ParserNode {
916

1017
fileprivate var parsers: [Parser] = []
1118

19+
/** delegate to write to an output stream, defaults to stdout. */
20+
public var writeToOutStream: (_ s: String) -> Void = { s in
21+
print(s)
22+
}
23+
1224
public var defaultAction: (() -> Void)?
1325

1426
/** callback invoked after all child parsers where invoked */
1527
public var afterChildrenParsed: OnParsed?
1628

29+
/**
30+
* Create a Command.
31+
* - Parameters:
32+
* - aliases: all strings that should be matched by this parser, e.g. for a flag '-v' and '--verbose'
33+
* - description: description to be shown in the global help
34+
* (will also be used for generated help on the command, if `helpText` is not given)
35+
* - helpTest: help text for the generated help
36+
*/
1737
public init(name: String,
1838
aliases: [String] = [],
1939
description: String = "",
40+
helpText: String? = nil,
2041
stopToken: String? = "--",
2142
parsed: OnParsed? = nil,
2243
parsers: [Parser] = [],
44+
helpPrinted: @escaping () -> Void = {
45+
exit(0)
46+
},
2347
afterChildrenParsed: OnParsed? = nil) {
2448

2549
self.afterChildrenParsed = afterChildrenParsed
@@ -34,6 +58,22 @@ open class Command: ValueParser<Bool>, ParserNode {
3458
}
3559

3660
self.parsers = parsers
61+
62+
let printHelp: () -> Void = {
63+
[unowned self] in
64+
var rows: [[String]] = []
65+
self.flatMap({ $0.description })
66+
.forEach({ (argument: String, description: String) in
67+
rows.append([" ", argument, description])
68+
})
69+
self.writeToOutStream(
70+
"\(helpText ?? description)\n\(Help.createTable(rows))")
71+
helpPrinted()
72+
}
73+
// add help as first parse, to play together with the var arg parser
74+
Log.debug("creating generated help flag as first parser")
75+
insert(Help(longName: "help", shortName: "h", parsed: { _ in printHelp() }), at: 0)
76+
defaultAction = printHelp
3777
}
3878

3979
open override func parse(arguments: [String], atIndex i: Int, path: [ParsePathSegment]) throws -> Int {
@@ -119,6 +159,7 @@ public extension Command {
119159
parsers.removeSubrange(bounds)
120160
}
121161

162+
@discardableResult
122163
public func removeFirst() -> Parser {
123164
return parsers.removeFirst()
124165
}

Sources/parsers/Help.swift

+39-11
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,58 @@ public class Help: Flag {
55

66
/** create a simple ascii table */
77
public static func createTable(_ rows: [[String]]) -> String {
8-
var colMaxCount: [Int] = []
8+
var colCount = 0
9+
var colWidths: [Int] = []
10+
// number of lines per row
11+
var rowLines: [Int] = Array(repeating: 0, count: rows.count)
912
// determine the widths of the columns
10-
rows.forEach { row in
11-
row.enumerated().forEach { i, col in
12-
if colMaxCount.count <= i {
13-
colMaxCount.append(col.count)
13+
rows.enumerated().forEach { i, cols in
14+
colCount = max(colCount, cols.count)
15+
cols.enumerated().forEach { j, col in
16+
let lines = String(col).split(separator: "\n")
17+
rowLines[i] = max(rowLines[i], lines.count)
18+
let colCount = lines.map {
19+
$0.count
20+
}.max() ?? 0
21+
if colWidths.count <= j {
22+
colWidths.append(colCount)
1423
}
15-
colMaxCount[i] = max(colMaxCount[i], col.count)
24+
colWidths[j] = max(colWidths[j], col.count)
1625
}
1726
}
1827

1928
// set the last col count to zero, to avoid padding the last col
20-
if colMaxCount.popLast() != nil {
21-
colMaxCount.append(0)
29+
if colWidths.popLast() != nil {
30+
colWidths.append(0)
31+
}
32+
33+
// compute rows that account for multiline rows
34+
var adjustedRows: [[String]] = []
35+
for i in 0..<rows.count {
36+
for _ in 0..<rowLines[i] {
37+
adjustedRows.append(Array(repeating: "", count: colCount))
38+
}
39+
}
40+
41+
var i = 0
42+
rows.enumerated().forEach { j, cols in
43+
cols.enumerated().forEach { l, col in
44+
let lines = String(col).split(separator: "\n")
45+
lines.enumerated().forEach { k, line in
46+
adjustedRows[i + k][l] = String(line)
47+
}
48+
}
49+
i += rowLines[j]
2250
}
2351

2452
// format the columns
25-
let r = rows.map { row in
53+
let r = adjustedRows.map { row in
2654
return row
2755
.enumerated()
2856
.map({ i, col in
29-
if col.count < colMaxCount[i] {
57+
if col.count < colWidths[i] {
3058
// right pad texts with whitespace
31-
return col + String(repeating: " ", count: colMaxCount[i] - col.count)
59+
return col + String(repeating: " ", count: colWidths[i] - col.count)
3260
}
3361
return col
3462
}).joined(separator: " ")

Tests/argtreeTests/ArgTreeTests.swift

+43-13
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,10 @@ final class ArgTreeTests: XCTestCase {
8888
let tokensConsumed = try? argTree.parse(arguments: ["foo", "--help"])
8989
XCTAssertEqual(tokensConsumed, 2)
9090
let expected = """
91-
usage
92-
--help, -h print this help
93-
--bar, -b a bar flag
94-
"""
91+
usage
92+
--help, -h print this help
93+
--bar, -b a bar flag
94+
"""
9595
XCTAssertEqualTrimmingWhiteSpace(out, expected)
9696
XCTAssert(helpPrinted)
9797
}
@@ -112,9 +112,38 @@ final class ArgTreeTests: XCTestCase {
112112
let tokensConsumed = try? argTree.parse(arguments: ["foo", "--help"])
113113
XCTAssertEqual(tokensConsumed, 2)
114114
let expected = """
115-
usage
116-
--help, -h print this help
117-
"""
115+
usage
116+
--help, -h print this help
117+
"""
118+
XCTAssertEqualTrimmingWhiteSpace(out, expected)
119+
XCTAssert(helpPrinted)
120+
}
121+
122+
func testGeneratedHelpWithMultilineText() {
123+
var helpPrinted = false
124+
let argTree = ArgTree(description: "usage") {
125+
helpPrinted = true
126+
}
127+
argTree.append(Flag(longName: "bar", shortName: "b", description:
128+
"""
129+
bar is a nice flag
130+
baz also
131+
""", parsed: { _ in }))
132+
133+
var out = ""
134+
argTree.writeToOutStream = { s in
135+
print(s, to: &out)
136+
}
137+
138+
argTree.defaultAction = nil
139+
let tokensConsumed = try? argTree.parse(arguments: ["foo", "--help"])
140+
XCTAssertEqual(tokensConsumed, 2)
141+
let expected = """
142+
usage
143+
--help, -h print this help
144+
--bar, -b bar is a nice flag
145+
baz also
146+
"""
118147
XCTAssertEqualTrimmingWhiteSpace(out, expected)
119148
XCTAssert(helpPrinted)
120149
}
@@ -171,17 +200,18 @@ final class ArgTreeTests: XCTestCase {
171200

172201
try! argTree.parse(arguments: ["foo", "-h"])
173202
let expected = """
174-
foo
175-
--verbose, -v print verbose output
176-
--help, -h print this help
177-
"""
203+
foo
204+
--verbose, -v print verbose output
205+
--help, -h print this help
206+
"""
178207
XCTAssertEqualTrimmingWhiteSpace(out, expected)
179208
}
180209

181-
#if !os(macOS)
210+
#if !os(macOS)
182211
static var allTests = [
183212
("testGeneratedHelp", testGeneratedHelp),
184213
("testGeneratedHelpIgnoringFlagWithoutDescription", testGeneratedHelpIgnoringFlagWithoutDescription),
214+
("testGeneratedHelpWithMultilineText", testGeneratedHelpWithMultilineText),
185215
("testHelpLongFlag", testHelpLongFlag),
186216
("testHelpShortFlag", testHelpShortFlag),
187217
("testReorderingFlags", testReorderingFlags),
@@ -190,5 +220,5 @@ final class ArgTreeTests: XCTestCase {
190220
("testSimpleDemo", testSimpleDemo),
191221
("testVarArgsExample", testVarArgsExample),
192222
]
193-
#endif
223+
#endif
194224
}

0 commit comments

Comments
 (0)