├── .github
└── workflows
│ └── swift.yml
├── .gitignore
├── CarthageSupport
├── SupportingFiles
│ ├── Info.plist
│ └── SwiftCLI.h
└── SwiftCLI.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ └── xcschemes
│ └── SwiftCLI.xcscheme
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── SwiftCLI
│ ├── ArgumentList.swift
│ ├── ArgumentListManipulator.swift
│ ├── CLI.swift
│ ├── Command.swift
│ ├── Compatibility.swift
│ ├── CompletionGenerator.swift
│ ├── Error.swift
│ ├── HelpCommand.swift
│ ├── HelpMessageGenerator.swift
│ ├── Input.swift
│ ├── Option.swift
│ ├── OptionGroup.swift
│ ├── OptionRegistry.swift
│ ├── Parameter.swift
│ ├── Parser.swift
│ ├── Path.swift
│ ├── Stream.swift
│ ├── Task.swift
│ ├── Term.swift
│ ├── Validation.swift
│ ├── ValueBox.swift
│ └── VersionCommand.swift
├── SwiftCLI.podspec
└── Tests
├── Info.plist
├── LinuxMain.swift
└── SwiftCLITests
├── ArgumentListTests.swift
├── CompletionGeneratorTests.swift
├── Fixtures.swift
├── HelpMessageGeneratorTests.swift
├── InputTests.swift
├── OptionRegistryTests.swift
├── ParameterFillerTests.swift
├── ParserTests.swift
├── RouterTests.swift
├── StreamTests.swift
├── SwiftCLITests.swift
├── TaskTests.swift
├── ValidationTests.swift
└── XCTestManifests.swift
/.github/workflows/swift.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | strategy:
8 | matrix:
9 | os: [macOS-latest, ubuntu-latest]
10 | swift: ["5.4"]
11 | runs-on: ${{ matrix.os }}
12 | steps:
13 | - uses: actions/checkout@v2
14 | - uses: fwal/setup-swift@v1
15 | with:
16 | swift-version: ${{ matrix.swift }}
17 | - name: Build
18 | run: swift build
19 | - name: Run tests
20 | run: swift test
21 |
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .build
3 | /SwiftCLI.xcodeproj
4 |
--------------------------------------------------------------------------------
/CarthageSupport/SupportingFiles/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSHumanReadableCopyright
22 | Copyright © 2018 Jake Heiser. All rights reserved.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/CarthageSupport/SupportingFiles/SwiftCLI.h:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftCLI.h
3 | // SwiftCLI
4 | //
5 | // Created by Cihat Gündüz on 05.11.18.
6 | // Copyright © 2018 Jake Heiser. All rights reserved.
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for SwiftCLI.
12 | FOUNDATION_EXPORT double SwiftCLIVersionNumber;
13 |
14 | //! Project version string for SwiftCLI.
15 | FOUNDATION_EXPORT const unsigned char SwiftCLIVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 |
20 |
--------------------------------------------------------------------------------
/CarthageSupport/SwiftCLI.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/CarthageSupport/SwiftCLI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/CarthageSupport/SwiftCLI.xcodeproj/xcshareddata/xcschemes/SwiftCLI.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Jake Heiser
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // Managed by ice
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SwiftCLI",
8 | products: [
9 | .library(name: "SwiftCLI", targets: ["SwiftCLI"]),
10 | ],
11 | targets: [
12 | .target(name: "SwiftCLI", dependencies: []),
13 | .testTarget(name: "SwiftCLITests", dependencies: ["SwiftCLI"]),
14 | ]
15 | )
16 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/ArgumentList.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArgumentList.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 3/28/17.
6 | // Copyright (c) 2017 jakeheis. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public class ArgumentList {
12 |
13 | private var storage: [String]
14 |
15 | /// Creates a list of the arguments from given array
16 | public init(arguments: [String]) {
17 | self.storage = arguments
18 | }
19 |
20 | /// Checks if list has another argument
21 | ///
22 | /// - Returns: whether list has another argument
23 | public func hasNext() -> Bool {
24 | return !storage.isEmpty
25 | }
26 |
27 | /// Pops off the next argument
28 | ///
29 | /// - Returns: the next argument
30 | /// - Precondition: list must not be empty
31 | @discardableResult
32 | public func pop() -> String {
33 | return storage.removeFirst()
34 | }
35 |
36 | /// Peeks at the next argument
37 | ///
38 | /// - Returns: the next argument
39 | /// - Precondition: list must not be empty
40 | @discardableResult
41 | public func peek() -> String {
42 | return storage[0]
43 | }
44 |
45 | /// Checks if the next argument is an option argument
46 | ///
47 | /// - Returns: whether next argument is an option
48 | public func nextIsOption() -> Bool {
49 | return storage.first?.hasPrefix("-") ?? false
50 | }
51 |
52 | /// Manipulate the argument list with the given closure
53 | ///
54 | /// - Parameter manipiulation: closure which takes in current array of arguments, returns manipulated array of args
55 | public func manipulate(_ manipiulation: ([String]) -> [String]) {
56 | storage = manipiulation(storage)
57 | }
58 |
59 | }
60 |
61 | extension ArgumentList: CustomStringConvertible {
62 | public var description: String {
63 | return storage.description
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/ArgumentListManipulator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArgumentListManipulator.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 3/28/17.
6 | // Copyright © 2017 jakeheis. All rights reserved.
7 | //
8 |
9 | public protocol _ArgumentListManipulator {
10 | func manipulate(arguments: ArgumentList)
11 | }
12 |
13 | @available(*, deprecated, message: "use a custom ParserResponse instead")
14 | public protocol ArgumentListManipulator: _ArgumentListManipulator {
15 | func manipulate(arguments: ArgumentList)
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/CLI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CLI.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 7/20/14.
6 | // Copyright (c) 2014 jakeheis. All rights reserved.
7 | //
8 |
9 | #if os(Linux)
10 | import Glibc
11 | #else
12 | import Darwin
13 | #endif
14 |
15 | public class CLI {
16 |
17 | /// The name of the CLI; used in help messages
18 | public let name: String
19 |
20 | /// The version of the CLI; if non-nil, a VersionCommand is automatically created
21 | public let version: String?
22 |
23 | /// The description of the CLI; used in help messages
24 | public var description: String?
25 |
26 | /// The array of commands (or command groups)
27 | public var commands: [Routable]
28 |
29 | /// A built-in help command; set to nil if this functionality is not wanted
30 | public lazy var helpCommand: HelpCommand? = HelpCommand(cli: self)
31 |
32 | /// A built-in version command; set to nil if this functionality is not wanted
33 | public lazy var versionCommand: VersionCommand? = {
34 | if let version = version {
35 | return VersionCommand(version: version)
36 | }
37 | return nil
38 | }()
39 |
40 | /// Options which every command inherits
41 | public var globalOptions: [Option] = []
42 |
43 | /// Option groups which every command inherits
44 | public var globalOptionGroups: [OptionGroup] = []
45 |
46 | /// A built-in help flag which each command automatically inherits; set to nil if this functionality is not wanted
47 | public var helpFlag: Flag? = Flag("-h", "--help", description: "Show help information")
48 |
49 | /// A map of command name aliases; by default, default maps "--version" to 'version'
50 | public var aliases: [String : String] = [
51 | "--version": "version"
52 | ]
53 |
54 | public var helpMessageGenerator: HelpMessageGenerator = DefaultHelpMessageGenerator()
55 | public var argumentListManipulators: [_ArgumentListManipulator] = []
56 | public var parser = Parser()
57 |
58 | /// Creates a new CLI
59 | ///
60 | /// - Parameter name: the name of the CLI executable
61 | /// - Parameter version: the current version of the CLI
62 | /// - Parameter description: a brief description of the CLI
63 | public init(name: String, version: String? = nil, description: String? = nil, commands: [Routable] = []) {
64 | self.name = name
65 | self.version = version
66 | self.description = description
67 | self.commands = commands
68 | }
69 |
70 | /// Create a single-command CLI; useful for example if you were implementing the 'ln' command
71 | ///
72 | /// - Parameter singleCommand: the single command
73 | public convenience init(singleCommand: Command) {
74 | self.init(name: singleCommand.name, commands: [singleCommand])
75 | parser.routeBehavior = .automatically(singleCommand)
76 | }
77 |
78 | /// Kicks off the entire CLI process, routing to and executing the command specified by the passed arguments.
79 | /// Uses the arguments passed in the command line. Exits the program upon completion.
80 | ///
81 | /// - Returns: Never
82 | public func goAndExit() -> Never {
83 | let result = go()
84 | exit(result)
85 | }
86 |
87 | /// Kicks off the entire CLI process, routing to and executing the command specified by the passed arguments.
88 | /// Uses the arguments passed in the command line.
89 | ///
90 | /// - Returns: an Int32 representing the success of the CLI in routing to and executing the correct
91 | /// command. Usually should be passed to `exit(result)`
92 | @discardableResult
93 | public func go() -> Int32 {
94 | return go(with: ArgumentList(arguments: Array(CommandLine.arguments.dropFirst())))
95 | }
96 |
97 | /// Kicks off the entire CLI process, routing to and executing the command specified by the passed arguments.
98 | ///
99 | /// - Parameter arguments: the arguments to execute with; should not include CLI name (i.e. if you wanted to execute "greeter greet world", 'arguments' should be ["greet", "world"])
100 | /// - Returns: an Int32 representing the success of the CLI in routing to and executing the correct command. Usually should be passed to `exit(result)`
101 | @discardableResult
102 | public func go(with arguments: [String]) -> Int32 {
103 | return go(with: ArgumentList(arguments: arguments))
104 | }
105 |
106 | // MARK: - Internal/private
107 |
108 | func go(with arguments: ArgumentList) -> Int32 {
109 | argumentListManipulators.forEach { $0.manipulate(arguments: arguments) }
110 |
111 | var exitStatus: Int32 = 0
112 |
113 | do {
114 | let path = try parse(arguments: arguments)
115 | if helpFlag?.wrappedValue == true {
116 | helpMessageGenerator.writeUsageStatement(for: path, to: stdout)
117 | } else {
118 | try path.command.execute()
119 | }
120 | } catch let error as ProcessError {
121 | if let message = error.message {
122 | stderr <<< message
123 | }
124 | exitStatus = Int32(error.exitStatus)
125 | } catch let error {
126 | helpMessageGenerator.writeUnrecognizedErrorMessage(for: error, to: stderr)
127 | exitStatus = 1
128 | }
129 |
130 | return exitStatus
131 | }
132 |
133 | private func parse(arguments: ArgumentList) throws -> CommandPath {
134 | do {
135 | return try parser.parse(cli: self, arguments: arguments)
136 | } catch let error as RouteError {
137 | helpMessageGenerator.writeRouteErrorMessage(for: error, to: stderr)
138 | throw CLI.Error()
139 | } catch let error as OptionError {
140 | if let command = error.command, command.command is HelpCommand {
141 | return command
142 | }
143 |
144 | helpMessageGenerator.writeMisusedOptionsStatement(for: error, to: stderr)
145 | throw CLI.Error()
146 | } catch let error as ParameterError {
147 | if error.command.command is HelpCommand || helpFlag?.wrappedValue == true {
148 | return error.command
149 | }
150 |
151 | helpMessageGenerator.writeParameterErrorMessage(for: error, to: stderr)
152 | throw CLI.Error()
153 | }
154 | }
155 |
156 | }
157 |
158 | extension CLI: CommandGroup {
159 |
160 | public var shortDescription: String {
161 | return description ?? ""
162 | }
163 |
164 | public var longDescription: String {
165 | return description ?? ""
166 | }
167 |
168 | public var children: [Routable] {
169 | var extra: [Routable] = []
170 | if let helpCommand = helpCommand {
171 | extra.append(helpCommand)
172 | }
173 | if let versionCommand = versionCommand {
174 | extra.append(versionCommand)
175 | }
176 | return commands + extra
177 | }
178 |
179 | public var options: [Option] {
180 | if let helpFlag = helpFlag {
181 | return globalOptions + [helpFlag]
182 | }
183 | return globalOptions
184 | }
185 |
186 | public var optionGroups: [OptionGroup] {
187 | return globalOptionGroups
188 | }
189 |
190 | }
191 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Command.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Command.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 7/11/14.
6 | // Copyright (c) 2014 jakeheis. All rights reserved.
7 | //
8 |
9 | // MARK: - Routables
10 |
11 | public protocol Routable: AnyObject {
12 | /// The name of the command or command group
13 | var name: String { get }
14 |
15 | /// A concise description of what this command or group is
16 | var shortDescription: String { get }
17 |
18 | /// A longer description of how to use this command or group
19 | var longDescription: String { get }
20 |
21 | /// The options this command accepts; dervied automatically, don't implement unless custom functionality needed
22 | var options: [Option] { get }
23 |
24 | /// The option groups of this command; defaults to empty array
25 | var optionGroups: [OptionGroup] { get }
26 | }
27 |
28 | extension Routable {
29 |
30 | /// Standard out stream
31 | public var stdout: WritableStream {
32 | return Term.stdout
33 | }
34 |
35 | /// Standard error stream
36 | public var stderr: WritableStream {
37 | return Term.stderr
38 | }
39 |
40 | public var options: [Option] {
41 | return optionsFromMirror(Mirror(reflecting: self))
42 | }
43 |
44 | private func optionsFromMirror(_ mirror: Mirror) -> [Option] {
45 | var options: [Option] = []
46 | if let superMirror = mirror.superclassMirror {
47 | options = optionsFromMirror(superMirror)
48 | }
49 |
50 | options.append(contentsOf: mirror.children.compactMap { (child) in
51 | #if !os(macOS)
52 | #if swift(>=4.1.50)
53 | print(child.label as Any, to: &NoStream.stream)
54 | guard child.label != "children" && child.label != "optionGroups" else {
55 | return nil
56 | }
57 | #endif
58 | #endif
59 |
60 | return child.value as? Option
61 | })
62 |
63 | return options
64 | }
65 |
66 | public var optionGroups: [OptionGroup] {
67 | return []
68 | }
69 | }
70 |
71 | // MARK: -
72 |
73 | public protocol Command: Routable {
74 |
75 | /// Executes the command
76 | ///
77 | /// - Throws: CLI.Error if command cannot execute successfully
78 | func execute() throws
79 |
80 | /// The parameters this command accepts; derived automatically, don't implement unless custom functionality needed
81 | var parameters: [NamedParameter] { get }
82 |
83 | }
84 |
85 | extension Command {
86 |
87 | public var parameters: [NamedParameter] {
88 | return parametersFromMirror(Mirror(reflecting: self))
89 | }
90 |
91 | private func parametersFromMirror(_ mirror: Mirror) -> [NamedParameter] {
92 | var parameters: [NamedParameter] = []
93 | if let superMirror = mirror.superclassMirror {
94 | parameters = parametersFromMirror(superMirror)
95 | }
96 | parameters.append(contentsOf: mirror.children.compactMap { (child) in
97 | guard var label = child.label else {
98 | return nil
99 | }
100 |
101 | #if !os(macOS)
102 | #if swift(>=4.1.50)
103 | print("label \(label)", to: &NoStream.stream)
104 | print("label \(label)", to: &NoStream.stream)
105 | guard label != "children" && label != "optionGroups" else {
106 | return nil
107 | }
108 | #endif
109 | #endif
110 |
111 | if let param = child.value as? AnyParameter {
112 | if label.hasPrefix("_") {
113 | label = String(label[label.index(after: label.startIndex)...])
114 | }
115 | return NamedParameter(name: label, param: param)
116 | }
117 | return nil
118 | })
119 | return parameters
120 | }
121 |
122 | public var shortDescription: String {
123 | return ""
124 | }
125 |
126 | public var longDescription: String {
127 | return ""
128 | }
129 |
130 | }
131 |
132 | // MARK: -
133 |
134 | public protocol CommandGroup: Routable {
135 | /// The sub-commands and sub-groups of this group
136 | var children: [Routable] { get }
137 |
138 | /// Aliases for chlidren, e.g. "--help" for "help"; default empty dictionary
139 | var aliases: [String: String] { get }
140 | }
141 |
142 | extension CommandGroup {
143 | public var aliases: [String: String] {
144 | return [:]
145 | }
146 |
147 | public var longDescription: String {
148 | return ""
149 | }
150 | }
151 |
152 | #if !os(macOS)
153 | #if swift(>=4.1.50)
154 | struct NoStream: TextOutputStream {
155 |
156 | // Fix for strange crash on Linux with Swift >= 4.2
157 |
158 | static var stream = NoStream()
159 |
160 | mutating func write(_ string: String) {
161 | // No-op
162 | }
163 |
164 | }
165 | #endif
166 | #endif
167 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Compatibility.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Compatibility.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 9/9/17.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: Minor version deprecations
11 |
12 | @available(*, deprecated, message: "use Param")
13 | public typealias Parameter = Param
14 |
15 | @available(*, deprecated, message: "use Param")
16 | public typealias OptionalParameter = Param
17 |
18 | @available(*, deprecated, message: "use CollectedParam(minCount: 1)")
19 | public class CollectedParameter: CollectedParam {
20 | public init(completion: ShellCompletion = .filename, validation: [Validation] = []) {
21 | super.init(minCount: 1, completion: completion, validation: validation)
22 | }
23 | }
24 |
25 | @available(*, deprecated, message: "use CollectedParam")
26 | public typealias OptionalCollectedParameter = CollectedParam
27 |
28 | // MARK: -
29 |
30 | @available(*, unavailable, renamed: "Task.run")
31 | public func run(_ executable: String, _ arguments: String...) throws {}
32 |
33 | @available(*, unavailable, renamed: "Task.run")
34 | public func run(_ executable: String, arguments: [String], directory: String? = nil) throws {}
35 |
36 | @available(*, unavailable, renamed: "Task.capture")
37 | public func capture(_ executable: String, _ arguments: String...) throws -> CaptureResult { fatalError() }
38 |
39 | @available(*, unavailable, renamed: "Task.capture")
40 | public func capture(_ executable: String, arguments: [String], directory: String? = nil) throws -> CaptureResult { fatalError() }
41 |
42 | @available(*, unavailable, renamed: "Task.run")
43 | public func run(bash: String, directory: String? = nil) throws {}
44 |
45 | @available(*, unavailable, renamed: "Task.capture")
46 | public func capture(bash: String, directory: String? = nil) throws -> CaptureResult { fatalError() }
47 |
48 | #if !os(iOS)
49 |
50 | extension Task {
51 |
52 | @available(*, unavailable, message: "Use Task.execvp(_:arguments:directory:env) instead")
53 | public static func execvp(_ executable: String, directory: String? = nil, _ args: String..., env: [String: String]? = nil) throws -> Never {
54 | fatalError()
55 | }
56 |
57 | @available(*, unavailable, message: "Use Task.execvp(_:arguments:directory:env) instead")
58 | public static func execvp(_ executable: String, directory: String? = nil, _ args: [String], env: [String: String]? = nil) throws -> Never {
59 | fatalError()
60 | }
61 |
62 | @available(*, unavailable, renamed: "init(executable:arguments:directory:stdout:stderr:stdin:)")
63 | public convenience init(executable: String, args: [String] = [], currentDirectory: String? = nil, stdout: WritableStream = WriteStream.stdout, stderr: WritableStream = WriteStream.stderr, stdin: ReadableStream = ReadStream.stdin) {
64 | fatalError()
65 | }
66 |
67 | @available(*, unavailable)
68 | public static func findExecutable(named: String) -> String? { nil }
69 |
70 | }
71 |
72 | #endif
73 |
74 | extension Input {
75 |
76 | @available(*, unavailable, message: "Use Validation.custom instead of (String) -> Bool")
77 | public static func readLine(prompt: String? = nil, secure: Bool = false, validation: ((String) -> Bool)? = nil, errorResponse: InputReader.ErrorResponse? = nil) -> String {
78 | return ""
79 | }
80 |
81 | @available(*, unavailable, message: "Use Validation.custom instead of (String) -> Bool")
82 | public static func readInt(prompt: String? = nil, secure: Bool = false, validation: ((Int) -> Bool)? = nil, errorResponse: InputReader.ErrorResponse? = nil) -> Int {
83 | return 0
84 | }
85 |
86 | @available(*, unavailable, message: "Use Validation.custom instead of (String) -> Bool")
87 | public static func readDouble(prompt: String? = nil, secure: Bool = false, validation: ((Double) -> Bool)? = nil, errorResponse: InputReader.ErrorResponse? = nil) -> Double {
88 | return 0
89 | }
90 |
91 | @available(*, unavailable, message: "Use Validation.custom instead of (String) -> Bool")
92 | public static func readBool(prompt: String? = nil, secure: Bool = false, validation: ((Bool) -> Bool)? = nil, errorResponse: InputReader.ErrorResponse? = nil) -> Bool {
93 | return false
94 | }
95 |
96 | @available(*, unavailable, message: "Use Validation.custom instead of (T) -> Bool")
97 | public static func readObject(prompt: String? = nil, secure: Bool = false, validation: ((T) -> Bool)? = nil, errorResponse: InputReader.ErrorResponse? = nil) -> T {
98 | fatalError()
99 | }
100 |
101 | }
102 |
103 | extension InputReader {
104 | @available(*, unavailable, message: "Use Validation.custom instead of InputReader.Validation")
105 | public typealias Validation = (T) -> Bool
106 | }
107 |
108 | extension CLI {
109 |
110 | @available(*, unavailable, message: "Use go(with:)")
111 | public func debugGo(with argumentString: String) -> Int32 {
112 | return 1
113 | }
114 |
115 | }
116 |
117 | @available(*, unavailable, renamed: "ShellCompletion")
118 | public typealias Completion = ShellCompletion
119 |
120 | extension CaptureResult {
121 |
122 | /// The full stdout contents; use `stdout` for trimmed contents
123 | @available(*, unavailable, message: "Use stdout or stdoutData")
124 | public var rawStdout: String {
125 | return String(data: stdoutData, encoding: .utf8) ?? ""
126 | }
127 |
128 | /// The full stderr contents; use `stderr` for trimmed contents
129 | @available(*, unavailable, message: "Use stderr or stderrData")
130 | public var rawStderr: String {
131 | return String(data: stderrData, encoding: .utf8) ?? ""
132 | }
133 |
134 | }
135 |
136 | // MARK: - Swift versions
137 |
138 | #if !swift(>=4.1)
139 |
140 | extension Sequence {
141 | func compactMap(_ transform: (Element) -> T?) -> [T] {
142 | return flatMap(transform)
143 | }
144 | }
145 |
146 | #endif
147 |
148 | #if !swift(>=5.0)
149 |
150 | extension Collection {
151 | func firstIndex(where test: (Element) -> Bool) -> Index? {
152 | return index(where: test)
153 | }
154 | }
155 |
156 | extension Collection where Element: Equatable {
157 | func firstIndex(of element: Element) -> Index? {
158 | return index(of: element)
159 | }
160 | }
161 |
162 | #endif
163 |
164 | // MARK: Unavailable
165 |
166 | extension CLI {
167 |
168 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
169 | public static var name: String { return "" }
170 |
171 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
172 | public static var version: String? { return nil }
173 |
174 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
175 | public static var description: String? { return "" }
176 |
177 | @available(*, unavailable, message: "Create a custom HelpMessageGenerator instead")
178 | public static var helpCommand: Command? = nil
179 |
180 | @available(*, unavailable, message: "Create the CLI object with a nil version and register a custom version command")
181 | public static var versionCommand: Command? = nil
182 |
183 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
184 | public static var helpMessageGenerator: HelpMessageGenerator { return DefaultHelpMessageGenerator() }
185 |
186 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
187 | public static var argumentListManipulators: [_ArgumentListManipulator] { return [] }
188 |
189 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
190 | public static func setup(name: String, version: String? = nil, description: String? = nil) {}
191 |
192 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
193 | public static func register(command: Command) {}
194 |
195 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
196 | public static func register(commands: [Command]) {}
197 |
198 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
199 | public static func go() -> Int32 { return 0 }
200 |
201 | @available(*, unavailable, message: "Create a new CLI object: let cli = CLI(..)")
202 | public static func debugGo(with argumentString: String) -> Int32 { return 0 }
203 |
204 | }
205 |
206 | extension Input {
207 |
208 | @available(*, unavailable, message: "Use Input.readLine()")
209 | public static func awaitInput(message: String?, secure: Bool = false) -> String { return "" }
210 |
211 | @available(*, unavailable, message: "Use Input.readLine() with a validation closure")
212 | public static func awaitInputWithValidation(message: String?, secure: Bool = false, validation: (_ input: String) -> Bool) -> String { return "" }
213 |
214 | @available(*, unavailable, message: "Implement CovertibleFromString on a custom object and use Input.readObject()")
215 | public static func awaitInputWithConversion(message: String?, secure: Bool = false, conversion: (_ input: String) -> T?) -> T { return conversion("")! }
216 |
217 | @available(*, unavailable, message: "Use Input.readInt() instead")
218 | public static func awaitInt(message: String?) -> Int { return 0 }
219 |
220 | @available(*, unavailable, message: "Use Input.readBool() instead")
221 | public static func awaitYesNoInput(message: String = "Confirm?") -> Bool { return false }
222 |
223 | }
224 |
225 | @available(*, unavailable, renamed: "WritableStream")
226 | public typealias OutputByteStream = WritableStream
227 |
228 | @available(*, unavailable, message: "Use WriteStream.stdout instead")
229 | public class StdoutStream {}
230 |
231 | @available(*, unavailable, message: "Use WriteStream.stderr instead")
232 | public class StderrStream {}
233 |
234 | @available(*, unavailable, message: "Use WriteStream.null instead")
235 | public class NullStream {}
236 |
237 | @available(*, unavailable, renamed: "WriteStream")
238 | public typealias FileStream = WriteStream
239 |
240 | extension WritableStream {
241 |
242 | @available(*, unavailable, renamed: "print")
243 | func output(_ content: String) {}
244 |
245 | @available(*, unavailable, renamed: "print")
246 | public func output(_ content: String, terminator: String) {}
247 |
248 | }
249 |
250 | @available(*, unavailable, message: "use CLI.Error instead")
251 | public enum CLIError: Error {
252 | case error(String)
253 | case emptyError
254 | }
255 |
256 | @available(*, unavailable, message: "Implement HelpMessageGenerator instead")
257 | public protocol UsageStatementGenerator {
258 | func generateUsageStatement(for command: Command) -> String
259 | }
260 |
261 | @available(*, unavailable, renamed: "WriteStream.stderr.print")
262 | public func printError(_ error: String) {}
263 |
264 | @available(*, unavailable, renamed: "WriteStream.stderr.print")
265 | public func printError(_ error: String, terminator: String) {}
266 |
267 | extension WriteStream {
268 | @available(*, unavailable, renamed: "WriteStream.for(path:)")
269 | public init?(path: String) {
270 | return nil
271 | }
272 |
273 | @available(*, unavailable, renamed: "WriteStream.for(fileHandle:)")
274 | public init(writeHandle: FileHandle) {
275 | fatalError()
276 | }
277 |
278 | @available(*, unavailable, renamed: "closeWrite")
279 | public func close() {}
280 | }
281 |
282 | extension WriteStream.FileStream {
283 | @available(*, unavailable, renamed: "closeWrite")
284 | public func close() {}
285 | }
286 |
287 | extension WriteStream.FileHandleStream {
288 | @available(*, unavailable, renamed: "closeWrite")
289 | public func close() {}
290 | }
291 |
292 | extension ReadStream {
293 | @available(*, unavailable, renamed: "ReadStream.for(path:)")
294 | public init?(path: String) {
295 | return nil
296 | }
297 |
298 | @available(*, unavailable, renamed: "ReadStream.for(fileHandle:)")
299 | public init(readHandle: FileHandle) {
300 | fatalError()
301 | }
302 |
303 | @available(*, unavailable, renamed: "closeRead")
304 | public func close() {}
305 | }
306 |
307 | extension ReadStream.FileStream {
308 | @available(*, unavailable, renamed: "closeRead")
309 | public func close() {}
310 | }
311 |
312 | extension ReadStream.FileHandleStream {
313 | @available(*, unavailable, renamed: "closeRead")
314 | public func close() {}
315 | }
316 |
317 | extension LineStream {
318 | @available(*, unavailable, message: "no longer needs to be called if this stream is the stdout or stderr of a Task; otherwise call waitToFinishProcessing()")
319 | public func wait() {}
320 | }
321 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/CompletionGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompletionGenerator.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 9/10/17.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum ShellCompletion {
11 | case none
12 | case filename
13 | case values([(name: String, description: String)])
14 | case function(String)
15 | }
16 |
17 | public protocol CompletionGenerator {
18 | init(cli: CLI)
19 | init(cli: CLI, functions: [String: String])
20 | func writeCompletions()
21 | func writeCompletions(into stream: WritableStream)
22 | }
23 |
24 | public final class ZshCompletionGenerator: CompletionGenerator {
25 |
26 | public let cli: CLI
27 | public let functions: [String: String]
28 |
29 | public convenience init(cli: CLI) {
30 | self.init(cli: cli, functions: [:])
31 | }
32 |
33 | public init(cli: CLI, functions: [String: String]) {
34 | self.cli = cli
35 | self.functions = functions
36 | }
37 |
38 | public func writeCompletions() {
39 | writeCompletions(into: Term.stdout)
40 | }
41 |
42 | public func writeCompletions(into stream: WritableStream) {
43 | stream <<< """
44 | #compdef \(cli.name)
45 | local context state state_descr line
46 | typeset -A opt_args
47 |
48 | """
49 |
50 | writeGroup(for: CommandGroupPath(top: cli), into: stream)
51 |
52 | functions.forEach { writeFunction(name: $0.key, body: $0.value, into: stream) }
53 |
54 | stream <<< "_\(cli.name)"
55 | }
56 |
57 | func writeGroup(for group: CommandGroupPath, into stream: WritableStream) {
58 | let name = functionName(for: group)
59 |
60 | let options = genOptionArgs(for: group.bottom).joined(separator: " \\\n")
61 | let commandList = group.bottom.children.map { (child) in
62 | return " \"\(child.name):\(escapeDescription(child.shortDescription))\""
63 | }.joined(separator: "\n")
64 |
65 |
66 | let subcommands = group.bottom.children.map { (child) -> String in
67 | let indentation = " "
68 | return """
69 | \(indentation)(\(child.name))
70 | \(indentation) _\(functionName(for: group.appending(child)))
71 | \(indentation) ;;
72 | """
73 | }.joined(separator: "\n")
74 |
75 | stream <<< """
76 | _\(name)() {
77 | _arguments -C \\
78 | """
79 | if !options.isEmpty {
80 | stream <<< options + " \\"
81 | }
82 | stream <<< """
83 | '(-): :->command' \\
84 | '(-)*:: :->arg' && return
85 | case $state in
86 | (command)
87 | local commands
88 | commands=(
89 | \(commandList)
90 | )
91 | _describe 'command' commands
92 | ;;
93 | (arg)
94 | case ${words[1]} in
95 | \(subcommands)
96 | esac
97 | ;;
98 | esac
99 | }
100 | """
101 |
102 | group.bottom.children.forEach { (routable) in
103 | if let subGroup = routable as? CommandGroup {
104 | self.writeGroup(for: group.appending(subGroup), into: stream)
105 | } else if let command = routable as? Command {
106 | self.writeCommand(for: group.appending(command), into: stream)
107 | }
108 | }
109 | }
110 |
111 | func writeCompletion(_ completion: ShellCompletion) -> String {
112 | switch completion {
113 | case .filename:
114 | return "_files"
115 | case .none:
116 | return " "
117 | case .values(let vals):
118 | let valPortion = vals.map { (value) in
119 | var line = "'\(value.name)"
120 | if !value.description.isEmpty {
121 | line += "[\(value.description)]"
122 | }
123 | line += "'"
124 | return line
125 | }.joined(separator: " ")
126 | return "{_values '' \(valPortion)}"
127 | case .function(let function):
128 | return function
129 | }
130 | }
131 |
132 | func writeCommand(for command: CommandPath, into stream: WritableStream) {
133 | let optionArgs = genOptionArgs(for: command.command).joined(separator: " \\\n")
134 | let paramArgs = command.command.parameters.map { (namedParam) -> String in
135 | var line = " \""
136 | if namedParam.param is AnyCollectedParameter {
137 | line += "*"
138 | }
139 | line += ":"
140 | if !namedParam.param.required {
141 | line += ":"
142 | }
143 | line += "\(namedParam.name):\(writeCompletion(namedParam.param.completion))"
144 | line += "\""
145 | return line
146 | }.joined(separator: " \\\n")
147 |
148 | let name = functionName(for: command)
149 | stream <<< """
150 | _\(name)() {
151 | _arguments -C\(optionArgs.isEmpty && paramArgs.isEmpty ? "" : " \\")
152 | """
153 | if !optionArgs.isEmpty {
154 | stream <<< optionArgs + (paramArgs.isEmpty ? "" : " \\")
155 | }
156 | if !paramArgs.isEmpty {
157 | stream <<< paramArgs
158 | }
159 | stream <<< "}"
160 | }
161 |
162 | func writeFunction(name: String, body: String, into stream: WritableStream) {
163 | let lines = body.components(separatedBy: "\n").map { " " + $0 }.joined(separator: "\n")
164 | stream <<< """
165 | \(name)() {
166 | \(lines)
167 | }
168 | """
169 | }
170 |
171 | // MARK: - Helpers
172 |
173 | enum OptionWritingMode {
174 | case normal
175 | case additionalExclusive([String])
176 | case variadic
177 | }
178 |
179 | private func genOptionLine(names: [String], mode: OptionWritingMode, description: String, completion: ShellCompletion?) -> String {
180 | precondition(names.count > 0)
181 |
182 | var line = " "
183 |
184 | let mutuallyExclusive: [String]
185 | switch mode {
186 | case .normal:
187 | mutuallyExclusive = names.count > 1 ? names : []
188 | case .additionalExclusive(let exclusive): mutuallyExclusive = exclusive
189 | case .variadic:
190 | precondition(names.count == 1)
191 | mutuallyExclusive = []
192 | }
193 |
194 | if !mutuallyExclusive.isEmpty {
195 | line += "'(\(mutuallyExclusive.joined(separator: " ")))'"
196 | }
197 |
198 | if names.count > 1 {
199 | line += "{\(names.joined(separator: ","))}\"["
200 | } else {
201 | line += "\""
202 | if case .variadic = mode {
203 | line += "*"
204 | }
205 | line += "\(names[0])["
206 | }
207 |
208 | line += escapeDescription(description) + "]"
209 |
210 | if let completion = completion {
211 | line += ": :\(writeCompletion(completion))"
212 | }
213 |
214 | line += "\""
215 | return line
216 | }
217 |
218 | private func genOptionArgs(for routable: Routable) -> [String] {
219 | let lines = routable.options.map { (option) -> [String] in
220 | let completion: ShellCompletion?
221 | if let key = option as? AnyKey {
222 | completion = key.completion
223 | } else {
224 | completion = nil
225 | }
226 |
227 | if option.variadic {
228 | return option.names.map { (name) in
229 | return genOptionLine(names: [name], mode: .variadic, description: option.shortDescription, completion: completion)
230 | }
231 | }
232 | for group in routable.optionGroups where group.restriction != .atLeastOne {
233 | if group.options.contains(where: { $0 === option }) {
234 | let exclusive = Array(group.options.map({ $0.names }).joined())
235 | return [genOptionLine(names: option.names, mode: .additionalExclusive(exclusive), description: option.shortDescription, completion: completion)]
236 | }
237 | }
238 | return [genOptionLine(names: option.names, mode: .normal, description: option.shortDescription, completion: completion)]
239 | }
240 | return Array(lines.joined())
241 | }
242 |
243 | private func escapeDescription(_ description: String) -> String {
244 | return description.replacingOccurrences(of: "\"", with: "\\\"")
245 | }
246 |
247 | private func functionName(for routable: RoutablePath) -> String {
248 | return routable.joined(separator: "_")
249 | }
250 |
251 | }
252 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Error.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Error.swift
3 | // SwiftCLIPackageDescription
4 | //
5 | // Created by Jake Heiser on 8/30/17.
6 | //
7 |
8 | public protocol ProcessError: Swift.Error {
9 | var message: String? { get }
10 | var exitStatus: Int32 { get }
11 | }
12 |
13 | extension CLI {
14 |
15 | public struct Error: ProcessError {
16 |
17 | public let message: String?
18 | public let exitStatus: Int32
19 |
20 | public init() {
21 | self.init(exitStatus: 1)
22 | }
23 |
24 | public init(exitStatus: T) {
25 | self.init(message: nil, exitStatus: Int32(exitStatus))
26 | }
27 |
28 | public init(message: String) {
29 | self.init(message: message, exitStatus: 1)
30 | }
31 |
32 | public init(message: String?, exitStatus: Int32) {
33 | self.message = message
34 | self.exitStatus = exitStatus
35 | }
36 |
37 | }
38 |
39 | }
40 |
41 | // MARK: - Parse errors
42 |
43 | public struct RouteError: Swift.Error {
44 | public let partialPath: CommandGroupPath
45 | public let notFound: String?
46 |
47 | public init(partialPath: CommandGroupPath, notFound: String?) {
48 | self.partialPath = partialPath
49 | self.notFound = notFound
50 | }
51 | }
52 |
53 | public struct OptionError: Swift.Error {
54 |
55 | public enum Kind {
56 | case expectedValueAfterKey(String)
57 | case unrecognizedOption(String)
58 | case optionGroupMisuse(OptionGroup)
59 | case invalidKeyValue(AnyKey, String, InvalidValueReason)
60 | case unexpectedValueAfterFlag(String)
61 |
62 | public var message: String {
63 | switch self {
64 | case let .expectedValueAfterKey(key):
65 | return "expected a value to follow '\(key)'"
66 | case let .unrecognizedOption(opt):
67 | return "unrecognized option '\(opt)'"
68 | case let .optionGroupMisuse(group):
69 | let condition: String
70 | if group.options.count == 1 {
71 | condition = "must pass the following option"
72 | } else {
73 | switch group.restriction {
74 | case .exactlyOne:
75 | condition = "must pass exactly one of the following"
76 | case .atLeastOne:
77 | condition = "must pass at least one of the following"
78 | case .atMostOne:
79 | condition = "must not pass more than one of the following"
80 | }
81 | }
82 | return condition + ": \(group.options.compactMap({ $0.names.last }).joined(separator: " "))"
83 | case let .invalidKeyValue(key, id, reason):
84 | return key.valueType.messageForInvalidValue(reason: reason, for: id)
85 | case let .unexpectedValueAfterFlag(flag):
86 | return "unexpected value following flag '\(flag)'"
87 | }
88 | }
89 | }
90 |
91 | public let command: CommandPath?
92 | public let kind: Kind
93 |
94 | public init(command: CommandPath?, kind: Kind) {
95 | self.command = command
96 | self.kind = kind
97 | }
98 | }
99 |
100 | public struct ParameterError: Swift.Error {
101 |
102 | public enum Kind {
103 | case wrongNumber(Int, Int?)
104 | case invalidValue(NamedParameter, InvalidValueReason)
105 |
106 | public var message: String {
107 | switch self {
108 | case let .wrongNumber(min, max):
109 | let plural = min == 1 ? "argument" : "arguments"
110 | switch max {
111 | case .none:
112 | return "command requires at least \(min) \(plural)"
113 | case let .some(max) where max == min:
114 | return "command requires exactly \(max) \(plural)"
115 | case let .some(max):
116 | return "command requires between \(min) and \(max) arguments"
117 | }
118 | case let .invalidValue(param, reason):
119 | return param.param.valueType.messageForInvalidValue(reason: reason, for: param.name)
120 | }
121 | }
122 | }
123 |
124 | public let command: CommandPath
125 | public let kind: Kind
126 |
127 | public init(command: CommandPath, kind: Kind) {
128 | self.command = command
129 | self.kind = kind
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/HelpCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelpCommand.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 7/25/14.
6 | // Copyright (c) 2014 jakeheis. All rights reserved.
7 | //
8 |
9 | public class HelpCommand: Command {
10 |
11 | public let name = "help"
12 | public let shortDescription = "Prints help information"
13 |
14 | @CollectedParam(completion: .none)
15 | var command: [String]
16 |
17 | let cli: CLI
18 |
19 | init(cli: CLI) {
20 | self.cli = cli
21 | }
22 |
23 | public func execute() throws {
24 | var path = CommandGroupPath(top: cli)
25 |
26 | for pathSegment in command {
27 | let child = path.bottom.children.first(where: { $0.name == pathSegment })
28 | if let group = child as? CommandGroup {
29 | path = path.appending(group)
30 | } else if let command = child as? Command {
31 | cli.helpMessageGenerator.writeUsageStatement(for: path.appending(command), to: stdout)
32 | return
33 | } else {
34 | break
35 | }
36 | }
37 |
38 | cli.helpMessageGenerator.writeCommandList(for: path, to: stdout)
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/HelpMessageGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HelpMessageGenerator.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 6/13/15.
6 | // Copyright © 2015 jakeheis. All rights reserved.
7 | //
8 |
9 | public protocol HelpMessageGenerator {
10 | func writeCommandList(for path: CommandGroupPath, to out: WritableStream)
11 | func writeUsageStatement(for path: CommandPath, to out: WritableStream)
12 | func writeRouteErrorMessage(for error: RouteError, to out: WritableStream)
13 | func writeMisusedOptionsStatement(for error: OptionError, to out: WritableStream)
14 | func writeParameterErrorMessage(for error: ParameterError, to out: WritableStream)
15 | func writeUnrecognizedErrorMessage(for error: Error, to out: WritableStream)
16 | func writeErrorLine(for errorMessage: String, to out: WritableStream)
17 | }
18 |
19 | extension HelpMessageGenerator {
20 |
21 | public func writeCommandList(for path: CommandGroupPath, to out: WritableStream) {
22 | out <<< ""
23 | out <<< "Usage: \(path.joined()) [options]"
24 |
25 | let bottom = path.bottom
26 | if !bottom.shortDescription.isEmpty {
27 | out <<< ""
28 | out <<< bottom.shortDescription
29 | }
30 |
31 | var commandGroups: [CommandGroup] = []
32 | var commands: [Command] = []
33 | var maxNameLength = 12
34 | for routable in bottom.children {
35 | if let commandGroup = routable as? CommandGroup {
36 | commandGroups.append(commandGroup)
37 | } else if let command = routable as? Command {
38 | commands.append(command)
39 | }
40 | if routable.name.count > maxNameLength {
41 | maxNameLength = routable.name.count
42 | }
43 | }
44 |
45 | func write(_ routable: Routable) {
46 | let spacing = String(repeating: " ", count: maxNameLength + 4 - routable.name.count)
47 | let multilineSpacing = String(repeating: " ", count: maxNameLength + 4 + 2)
48 | let description = routable.shortDescription.replacingOccurrences(of: "\n", with: "\n\(multilineSpacing)")
49 | out <<< " \(routable.name)\(spacing)\(description)"
50 | }
51 |
52 | if !commandGroups.isEmpty {
53 | out <<< ""
54 | out <<< "Groups:"
55 | commandGroups.forEach(write)
56 | }
57 |
58 | if !commands.isEmpty {
59 | out <<< ""
60 | out <<< "Commands:"
61 | commands.forEach(write)
62 | }
63 | out <<< ""
64 | }
65 |
66 | public func writeUsageStatement(for path: CommandPath, to out: WritableStream) {
67 | out <<< ""
68 |
69 | out <<< "Usage: \(path.usage)"
70 |
71 | if !path.command.longDescription.isEmpty {
72 | out <<< ""
73 | out <<< path.command.longDescription
74 | } else if !path.command.shortDescription.isEmpty {
75 | out <<< ""
76 | out <<< path.command.shortDescription
77 | }
78 |
79 | let options = path.options
80 | if !options.isEmpty {
81 | out <<< ""
82 | out <<< "Options:"
83 | let sortedOptions = options.sorted { $0.names.last!.lowercased() < $1.names.last!.lowercased() }
84 | let maxOptionLength = sortedOptions.reduce(12) { max($0, $1.indentedIdentifierLength) }
85 | sortedOptions.forEach { (option) in
86 | let usage = option.usage(padding: maxOptionLength + 4)
87 | out <<< " \(usage)".replacingOccurrences(of: "\n", with: "\n ")
88 | }
89 | }
90 | out <<< ""
91 | }
92 |
93 | public func writeRouteErrorMessage(for error: RouteError, to out: WritableStream) {
94 | writeCommandList(for: error.partialPath, to: out)
95 | if let notFound = error.notFound {
96 | writeErrorLine(for: "command '\(notFound)' not found", to: out)
97 | out <<< ""
98 | }
99 | }
100 |
101 | public func writeMisusedOptionsStatement(for error: OptionError, to out: WritableStream) {
102 | if let command = error.command {
103 | writeUsageStatement(for: command, to: out)
104 | } else {
105 | out <<< ""
106 | }
107 | writeErrorLine(for: error.kind.message, to: out)
108 | out <<< ""
109 | }
110 |
111 | public func writeParameterErrorMessage(for error: ParameterError, to out: WritableStream) {
112 | writeUsageStatement(for: error.command, to: out)
113 | writeErrorLine(for: error.kind.message, to: out)
114 | out <<< ""
115 | }
116 |
117 | public func writeUnrecognizedErrorMessage(for error: Error, to out: WritableStream) {
118 | writeErrorLine(for: error.localizedDescription, to: out)
119 | }
120 |
121 | public func writeErrorLine(for errorMessage: String, to out: WritableStream) {
122 | out <<< "Error: " + errorMessage
123 | }
124 |
125 | }
126 |
127 | public class DefaultHelpMessageGenerator: HelpMessageGenerator {
128 |
129 | private enum Colors {
130 | static let escape = "\u{001B}["
131 | static let none = escape + "0m"
132 | static let red = escape + "31m"
133 | static let bold = escape + "1m"
134 | }
135 |
136 | public let colorError: Bool
137 | public let boldError: Bool
138 |
139 | public init(colorError: Bool = false, boldError: Bool = false) {
140 | self.colorError = colorError
141 | self.boldError = boldError
142 | }
143 |
144 | public func writeErrorLine(for errorMessage: String, to out: WritableStream) {
145 | var errorWord = ""
146 | if boldError {
147 | errorWord += Colors.bold
148 | }
149 | if colorError {
150 | errorWord += Colors.red
151 | }
152 |
153 | errorWord += "Error: "
154 | if colorError || boldError {
155 | errorWord += Colors.none
156 | }
157 | out <<< errorWord + errorMessage
158 | }
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Input.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Input.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 8/17/14.
6 | // Copyright (c) 2014 jakeheis. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public enum Input {
12 |
13 | /// Reads a line of input
14 | ///
15 | /// - Parameters:
16 | /// - prompt: prompt to be printed before accepting input (e.g. "Name: ")
17 | /// - secure: boolean defining that input should be hidden
18 | /// - validation: predicate defining whether the given input is valid
19 | /// - errorResponse: what to do if the input is invalid; default prints "Invalid input"
20 | /// - Returns: input
21 | public static func readLine(prompt: String? = nil, secure: Bool = false, validation: [Validation] = [], errorResponse: InputReader.ErrorResponse? = nil) -> String {
22 | return readObject(prompt: prompt, secure: secure, validation: validation, errorResponse: errorResponse)
23 | }
24 |
25 | /// Reads a line of input from stdin
26 | ///
27 | /// - Parameters:
28 | /// - prompt: prompt to be printed before accepting input (e.g. "Name: ")
29 | /// - defaultValue: value to fallback to for empty input; default is nil, meaning an error is shown
30 | /// - secure: boolean defining that input should be hidden
31 | /// - validation: predicate defining whether the given input is valid
32 | /// - errorResponse: what to do if the input is invalid; default prints "Invalid input"
33 | /// - Returns: input
34 | public static func readInt(prompt: String? = nil, defaultValue: Int? = nil, secure: Bool = false, validation: [Validation] = [], errorResponse: InputReader.ErrorResponse? = nil) -> Int {
35 | return readObject(prompt: prompt, defaultValue: defaultValue, secure: secure, validation: validation, errorResponse: errorResponse)
36 | }
37 |
38 | /// Reads a double from stdin
39 | ///
40 | /// - Parameters:
41 | /// - prompt: prompt to be printed before accepting input (e.g. "Name: ")
42 | /// - defaultValue: value to fallback to for empty input; default is nil, meaning an error is shown
43 | /// - secure: boolean defining that input should be hidden
44 | /// - validation: predicate defining whether the given input is valid
45 | /// - errorResponse: what to do if the input is invalid; default prints "Invalid input"
46 | /// - Returns: input
47 | public static func readDouble(prompt: String? = nil, defaultValue: Double? = nil, secure: Bool = false, validation: [Validation] = [], errorResponse: InputReader.ErrorResponse? = nil) -> Double {
48 | return readObject(prompt: prompt, defaultValue: defaultValue, secure: secure, validation: validation, errorResponse: errorResponse)
49 | }
50 |
51 | /// Reads a bool from stdin. "y", "yes", "t", and "true" are accepted as truthy values
52 | ///
53 | /// - Parameters:
54 | /// - prompt: prompt to be printed before accepting input (e.g. "Name: ")
55 | /// - defaultValue: value to fallback to for empty input; default is nil, meaning an error is shown
56 | /// - secure: boolean defining that input should be hidden
57 | /// - validation: predicate defining whether the given input is valid
58 | /// - errorResponse: what to do if the input is invalid; default prints "Invalid input"
59 | /// - Returns: input
60 | public static func readBool(prompt: String? = nil, defaultValue: Bool? = nil, secure: Bool = false, validation: [Validation] = [], errorResponse: InputReader.ErrorResponse? = nil) -> Bool {
61 | return readObject(prompt: prompt, defaultValue: defaultValue, secure: secure, validation: validation, errorResponse: errorResponse)
62 | }
63 |
64 | /// Reads an object which conforms to ConvertibleFromString from stdin
65 | ///
66 | /// - Parameters:
67 | /// - prompt: prompt to be printed before accepting input (e.g. "Name: ")
68 | /// - defaultValue: value to fallback to for empty input; default is nil, meaning an error is shown
69 | /// - secure: boolean defining that input should be hidden
70 | /// - validation: predicate defining whether the given input is valid
71 | /// - errorResponse: what to do if the input is invalid; default prints "Invalid input"
72 | /// - Returns: input
73 | public static func readObject(prompt: String? = nil, defaultValue: T? = nil, secure: Bool = false, validation: [Validation] = [], errorResponse: InputReader.ErrorResponse? = nil) -> T {
74 | return InputReader(prompt: prompt, defaultValue: defaultValue, secure: secure, validation: validation, errorResponse: errorResponse).read()
75 | }
76 |
77 | }
78 |
79 | // MARK: InputReader
80 |
81 | public class InputReader {
82 |
83 | public typealias ErrorResponse = (_ input: String, _ resaon: InvalidValueReason) -> ()
84 |
85 | public let prompt: String?
86 | public let defaultValue: T?
87 | public let secure: Bool
88 | public let validation: [SwiftCLI.Validation]
89 | public let errorResponse: ErrorResponse
90 |
91 | public init(prompt: String?, defaultValue: T?, secure: Bool, validation: [SwiftCLI.Validation], errorResponse: ErrorResponse?) {
92 | self.prompt = prompt
93 | self.defaultValue = defaultValue
94 | self.secure = secure
95 | self.validation = validation
96 | self.errorResponse = errorResponse ?? { (_, reason) in
97 | let message = T.messageForInvalidValue(reason: reason, for: nil)
98 | Term.stderr <<< String(message[message.startIndex]).capitalized + message[message.index(after: message.startIndex)...]
99 | }
100 | }
101 |
102 | public func read() -> T {
103 | while true {
104 | printPrompt()
105 |
106 | var possibleInput: String? = nil
107 | if secure {
108 | if let chars = UnsafePointer(getpass("")) {
109 | possibleInput = String(cString: chars, encoding: .utf8)
110 | }
111 | } else {
112 | possibleInput = Term.read()
113 | }
114 |
115 | guard let input = possibleInput else {
116 | // Eof reached; no way forward
117 | exit(1)
118 | }
119 |
120 | guard let converted = convert(input: input) else {
121 | errorResponse(input, .conversionError)
122 | continue
123 | }
124 |
125 | if let failedValidation = validation.first(where: { $0.validate(converted) == false }) {
126 | errorResponse(input, .validationError(failedValidation))
127 | continue
128 | }
129 |
130 | return converted
131 | }
132 | }
133 |
134 | private func convert(input: String) -> T? {
135 | if let defaultValue = defaultValue, input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
136 | return defaultValue
137 | }
138 | return T(input: input)
139 | }
140 |
141 | private func printPrompt() {
142 | if var prompt = prompt {
143 | if !prompt.hasSuffix(" ") && !prompt.hasSuffix("\n") {
144 | prompt += " "
145 | }
146 | Term.stdout.write(prompt)
147 | fflush(Foundation.stdout)
148 | }
149 | }
150 |
151 | }
152 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Option.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Option.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 3/28/17.
6 | // Copyright © 2017 jakeheis. All rights reserved.
7 | //
8 |
9 | public protocol Option: AnyObject, CustomStringConvertible {
10 | var names: [String] { get }
11 | var shortDescription: String { get }
12 | var identifier: String { get }
13 | var variadic: Bool { get }
14 | }
15 |
16 | extension Option {
17 |
18 | public var description: String {
19 | return "\(type(of: self))(\(identifier))"
20 | }
21 |
22 | public var indentedIdentifierLength: Int {
23 | if names.count == 1 && names[0].hasPrefix("--") { // no one letter shortcut; indent
24 | return identifier.count + 4
25 | }
26 | return identifier.count
27 | }
28 |
29 | public func usage(padding: Int) -> String {
30 | var id = identifier
31 | if names.count == 1 && names[0].hasPrefix("--") { // no one letter shortcut; indent
32 | id = " " + id
33 | }
34 | let spacing = String(repeating: " ", count: padding - id.count)
35 | let descriptionNewlineSpacing = String(repeating: " ", count: padding)
36 | let description = shortDescription.replacingOccurrences(of: "\n", with: "\n\(descriptionNewlineSpacing)")
37 | return "\(id)\(spacing)\(description)"
38 | }
39 |
40 | }
41 |
42 | // MARK: - Flags
43 |
44 | public protocol AnyFlag: Option {
45 | func update()
46 | }
47 |
48 | @propertyWrapper
49 | public class Flag: AnyFlag {
50 |
51 | public let names: [String]
52 | public let shortDescription: String
53 | public let variadic = false
54 |
55 | public private(set) var wrappedValue = false
56 | public var value: Bool { wrappedValue }
57 | public var projectedValue: Flag { self }
58 |
59 | public var identifier: String {
60 | return names.joined(separator: ", ")
61 | }
62 |
63 | /// Creates a new flag
64 | ///
65 | /// - Parameters:
66 | /// - names: the names for the flag; convention is to include a short name (-a) and a long name (--all)
67 | /// - description: A short description of what this flag does for usage statements
68 | public init(_ names: String..., description: String = "") {
69 | self.names = names.sorted(by: { $0.count < $1.count })
70 | self.shortDescription = description
71 | }
72 |
73 | /// Toggles the flag's value; don't call directly
74 | public func update() {
75 | wrappedValue = true
76 | }
77 |
78 | }
79 |
80 | @propertyWrapper
81 | public class CounterFlag: AnyFlag {
82 |
83 | public let names: [String]
84 | public let shortDescription: String
85 | public let variadic = true
86 |
87 | public private(set) var wrappedValue: Int = 0
88 | public var value: Int { wrappedValue }
89 | public var projectedValue: CounterFlag { self }
90 |
91 | public var identifier: String {
92 | return names.joined(separator: ", ")
93 | }
94 |
95 | /// Creates a new counter flag
96 | ///
97 | /// - Parameters:
98 | /// - names: the names for the flag; convention is to include a short name (-a) and a long name (--all)
99 | /// - description: A short description of what this flag does for usage statements
100 | public init(_ names: String ..., description: String = "") {
101 | self.names = names.sorted(by: { $0.count < $1.count })
102 | self.shortDescription = description
103 | }
104 |
105 | /// Increments the flag's value; don't call directly
106 | public func update() {
107 | wrappedValue += 1
108 | }
109 |
110 | }
111 |
112 | // MARK: - Keys
113 |
114 | public protocol AnyKey: Option, AnyValueBox {}
115 |
116 | public class _Key {
117 |
118 | public let names: [String]
119 | public let shortDescription: String
120 | public let completion: ShellCompletion
121 | public let validation: [Validation]
122 |
123 | public var identifier: String {
124 | return names.joined(separator: ", ") + " "
125 | }
126 |
127 | /// Creates a new key
128 | ///
129 | /// - Parameters:
130 | /// - names: the names for the key; convention is to include a short name (-m) and a long name (--message)
131 | /// - description: A short description of what this key does for usage statements
132 | public init(names: [String], description: String, completion: ShellCompletion, validation: [Validation] = []) {
133 | self.names = names.sorted(by: { $0.count < $1.count })
134 | self.shortDescription = description
135 | self.completion = completion
136 | self.validation = validation
137 | }
138 |
139 | }
140 |
141 | @propertyWrapper
142 | public class Key: _Key, AnyKey, ValueBox {
143 |
144 | public let variadic = false
145 |
146 | public private(set) var wrappedValue: Value?
147 | public var value: Value? { wrappedValue }
148 | public var projectedValue: Key { self }
149 |
150 | public init(_ names: String ..., description: String = "", completion: ShellCompletion = .filename, validation: [Validation] = []) {
151 | super.init(names: names, description: description, completion: completion, validation: validation)
152 | }
153 |
154 | public func update(to value: Value) {
155 | self.wrappedValue = value
156 | }
157 |
158 | }
159 |
160 | @propertyWrapper
161 | public class VariadicKey: _Key, AnyKey, ValueBox {
162 |
163 | public let variadic = true
164 |
165 | public private(set) var wrappedValue: [Value] = []
166 | public var value: [Value] { wrappedValue }
167 | public var projectedValue: VariadicKey { self }
168 |
169 | public init(_ names: String ..., description: String = "", completion: ShellCompletion = .filename, validation: [Validation] = []) {
170 | super.init(names: names, description: description, completion: completion, validation: validation)
171 | }
172 |
173 | public func update(to value: Value) {
174 | self.wrappedValue.append(value)
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/OptionGroup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OptionGroup.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 3/31/17.
6 | // Copyright © 2017 jakeheis. All rights reserved.
7 | //
8 |
9 | public class OptionGroup: CustomStringConvertible {
10 |
11 | public enum Restriction {
12 | case atMostOne // 0 or 1
13 | case exactlyOne // 1
14 | case atLeastOne // 1 or more
15 | }
16 |
17 | public static func atMostOne(_ options: Option...) -> OptionGroup {
18 | return .atMostOne(options)
19 | }
20 |
21 | public static func atMostOne(_ options: [Option]) -> OptionGroup {
22 | return .init(options: options, restriction: .atMostOne)
23 | }
24 |
25 | public static func exactlyOne(_ options: Option...) -> OptionGroup {
26 | return .exactlyOne(options)
27 | }
28 |
29 | public static func exactlyOne(_ options: [Option]) -> OptionGroup {
30 | return .init(options: options, restriction: .exactlyOne)
31 | }
32 |
33 | public static func atLeastOne(_ options: Option...) -> OptionGroup {
34 | return .atLeastOne(options)
35 | }
36 |
37 | public static func atLeastOne(_ options: [Option]) -> OptionGroup {
38 | return .init(options: options, restriction: .atLeastOne)
39 | }
40 |
41 | public let options: [Option]
42 | public let restriction: Restriction
43 | internal(set) public var count: Int = 0
44 |
45 | public var description: String {
46 | return "OptionGroup.\(restriction)(\(options))"
47 | }
48 |
49 | public init(options: [Option], restriction: Restriction) {
50 | precondition(!options.isEmpty, "must pass one or more options")
51 | if options.count == 1 {
52 | precondition(restriction == .exactlyOne, "cannot use atMostOne or atLeastOne when passing one option")
53 | }
54 |
55 | self.options = options
56 | self.restriction = restriction
57 | }
58 |
59 | public func check() -> Bool {
60 | if count == 0 && restriction != .atMostOne {
61 | return false
62 | }
63 | if count > 1 && restriction != .atLeastOne {
64 | return false
65 | }
66 | return true
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/OptionRegistry.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Options.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 7/11/14.
6 | // Copyright (c) 2014 jakeheis. All rights reserved.
7 | //
8 |
9 | public class OptionRegistry {
10 |
11 | private var flags: [String: AnyFlag]
12 | private var keys: [String: AnyKey]
13 | public private(set) var groups: [OptionGroup]
14 |
15 | public init(routable: Routable) {
16 | self.flags = [:]
17 | self.keys = [:]
18 | self.groups = []
19 |
20 | register(routable)
21 | }
22 |
23 | public func register(_ routable: Routable) {
24 | for option in routable.options {
25 | if let flag = option as? AnyFlag {
26 | for name in flag.names {
27 | flags[name] = flag
28 | }
29 | } else if let key = option as? AnyKey {
30 | for name in key.names {
31 | keys[name] = key
32 | }
33 | }
34 | }
35 |
36 | groups += routable.optionGroups
37 | }
38 |
39 | public func recognizesOption(_ opt: String) -> Bool {
40 | return flags[opt] != nil || keys[opt] != nil
41 | }
42 |
43 | // MARK: - Helpers
44 |
45 | public func flag(for key: String) -> AnyFlag? {
46 | if let flag = flags[key] {
47 | incrementCount(for: flag)
48 | return flag
49 | }
50 | return nil
51 | }
52 |
53 | public func key(for key: String) -> AnyKey? {
54 | if let key = keys[key] {
55 | incrementCount(for: key)
56 | return key
57 | }
58 | return nil
59 | }
60 |
61 | private func incrementCount(for option: Option) {
62 | for group in groups {
63 | if group.options.contains(where: { $0 === option }) {
64 | group.count += 1
65 | }
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Parameter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Parameter.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 3/7/15.
6 | // Copyright (c) 2015 jakeheis. All rights reserved.
7 | //
8 |
9 | public protocol AnyParameter: AnyValueBox {
10 | var required: Bool { get }
11 | var satisfied: Bool { get }
12 | }
13 |
14 | public class _Param {
15 |
16 | public let completion: ShellCompletion
17 | public let validation: [Validation]
18 |
19 | /// Creates a new parameter
20 | ///
21 | /// - Parameter completion: the completion type for use in ZshCompletionGenerator; default .filename
22 | fileprivate init(designatedCompletion: ShellCompletion = .filename, validation: [Validation] = []) {
23 | self.completion = designatedCompletion
24 | self.validation = validation
25 | }
26 |
27 | }
28 |
29 | @propertyWrapper
30 | public class Param : _Param, AnyParameter, ValueBox {
31 |
32 | public private(set) var required = true
33 | public var satisfied: Bool { privValue != nil }
34 |
35 | private var privValue: Value?
36 | public var wrappedValue: Value {
37 | guard let val = privValue else {
38 | fatalError("cannot access parameter value outside of 'execute' func")
39 | }
40 | return val
41 | }
42 | public var value: Value { wrappedValue }
43 | public var projectedValue: Param { self }
44 |
45 | fileprivate override init(designatedCompletion: ShellCompletion, validation: [Validation]) {
46 | super.init(designatedCompletion: designatedCompletion, validation: validation)
47 | }
48 |
49 | public init() {
50 | super.init()
51 | }
52 |
53 | public init(completion: ShellCompletion = .filename, validation: Validation...) {
54 | super.init(designatedCompletion: completion, validation: validation)
55 | }
56 |
57 | public func update(to value: Value) {
58 | self.privValue = value
59 | }
60 |
61 | }
62 |
63 | extension Param where Value: OptionType {
64 |
65 | public convenience init() {
66 | self.init(designatedCompletion: .filename, validation: [])
67 | setup()
68 | }
69 |
70 | public convenience init(completion: ShellCompletion) {
71 | self.init(designatedCompletion: completion, validation: [])
72 | setup()
73 | }
74 |
75 | public convenience init(completion: ShellCompletion = .filename, validation: Validation...) {
76 | let optValidations = validation.map { (valueValidation) in
77 | return Validation.custom(valueValidation.message) { (option) in
78 | if let value = option.swiftcli_Value {
79 | return valueValidation.validate(value)
80 | }
81 | return false
82 | }
83 | }
84 | self.init(designatedCompletion: completion, validation: optValidations)
85 | setup()
86 | }
87 |
88 | private func setup() {
89 | privValue = .some(.swiftcli_Empty)
90 | required = false
91 | }
92 |
93 | }
94 |
95 | public protocol AnyCollectedParameter: AnyParameter {
96 | var minCount: Int { get }
97 | }
98 |
99 | @propertyWrapper
100 | public class CollectedParam : _Param, AnyCollectedParameter, ValueBox {
101 |
102 | public var required: Bool { minCount > 0 }
103 | public var satisfied: Bool { value.count >= minCount }
104 |
105 | public private(set) var wrappedValue: [Value] = []
106 | public var value: [Value] { wrappedValue }
107 | public var projectedValue: CollectedParam {
108 | return self
109 | }
110 |
111 | public let minCount: Int
112 |
113 | public init() {
114 | self.minCount = 0
115 | super.init()
116 | }
117 |
118 | public init(minCount: Int = 0, completion: ShellCompletion = .filename, validation: [Validation] = []) {
119 | self.minCount = minCount
120 | super.init(designatedCompletion: completion, validation: validation)
121 | }
122 |
123 | public init(minCount: Int = 0, completion: ShellCompletion = .filename, validation: Validation...) {
124 | self.minCount = minCount
125 | super.init(designatedCompletion: completion, validation: validation)
126 | }
127 |
128 | public func update(to value: Value) {
129 | self.wrappedValue.append(value)
130 | }
131 |
132 | }
133 |
134 | // MARK: - NamedParameter
135 |
136 | public struct NamedParameter {
137 | public let name: String
138 | public let param: AnyParameter
139 |
140 | public var signature: String {
141 | var sig = "<\(name)>"
142 | if param.required == false {
143 | sig = "[\(sig)]"
144 | }
145 | if param is AnyCollectedParameter {
146 | sig += " ..."
147 | }
148 | return sig
149 | }
150 |
151 | public init(name: String, param: AnyParameter) {
152 | self.name = name
153 | self.param = param
154 | }
155 | }
156 |
157 | // MARK: - ParameterIterator
158 |
159 | public class ParameterIterator {
160 |
161 | private var params: [NamedParameter]
162 | private let collected: NamedParameter?
163 |
164 | public let minCount: Int
165 | public let maxCount: Int?
166 |
167 | public init(command: Command) {
168 | var all = command.parameters
169 |
170 | assert(all.firstIndex(where: { !$0.param.required }) ?? all.endIndex >= all.filter({ $0.param.required }).count, "optional parameters must come after all required parameters")
171 |
172 | var minCount = 0
173 |
174 | if let last = all.last, let collected = last.param as? AnyCollectedParameter {
175 | all.removeLast()
176 | assert(!all.contains(where: { $0.param is AnyCollectedParameter }), "can only have one collected parameter")
177 |
178 | self.collected = last
179 | self.maxCount = nil
180 | minCount = collected.minCount
181 | } else {
182 | assert(!all.contains(where: { $0.param is AnyCollectedParameter }), "the collected parameter must be the last parameter")
183 | self.collected = nil
184 | self.maxCount = all.count
185 | }
186 |
187 | self.minCount = all.filter({ $0.param.required }).count + minCount
188 | self.params = all
189 | }
190 |
191 | public func nextIsCollection() -> Bool {
192 | return params.isEmpty && collected != nil
193 | }
194 |
195 | public func next() -> NamedParameter? {
196 | if let individual = params.first {
197 | params.removeFirst()
198 | return individual
199 | }
200 | return collected
201 | }
202 |
203 | }
204 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Parser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Parser.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 6/28/16.
6 | // Copyright (c) 2016 jakeheis. All rights reserved.
7 | //
8 |
9 | // MARK: - Parser
10 |
11 | @dynamicMemberLookup
12 | public struct Parser {
13 |
14 | public struct Configuration {
15 | public enum RouteBehavior {
16 | case search
17 | case searchWithFallback(Command)
18 | case automatically(Command)
19 | }
20 |
21 | /// The route behavior to use to find the specified command; default .search
22 | public var routeBehavior: RouteBehavior = .search
23 |
24 | /// Continue parsing options after a collected parameter is encountered; default false
25 | public var parseOptionsAfterCollectedParameter = false
26 |
27 | /// Split options joined in one argument, e.g. split '-am' into '-a' and '-m'; default true
28 | public var splitJoinedOptions = true
29 |
30 | public var fallback: Command? {
31 | switch routeBehavior {
32 | case .search: return nil
33 | case .searchWithFallback(let cmd): return cmd
34 | case .automatically(let cmd): return cmd
35 | }
36 | }
37 | }
38 |
39 | public struct State {
40 |
41 | public enum RouteState {
42 | case routing(CommandGroupPath)
43 | case routed(CommandPath, ParameterIterator)
44 | }
45 |
46 | public var routeState: RouteState
47 | public let optionRegistry: OptionRegistry
48 | public let configuration: Configuration
49 |
50 | public var command: CommandPath? {
51 | if case let .routed(path, _) = routeState {
52 | return path
53 | }
54 | return nil
55 | }
56 |
57 | public mutating func appendToPath(_ group: CommandGroup) {
58 | guard case let .routing(current) = routeState else {
59 | assertionFailure()
60 | return
61 | }
62 |
63 | optionRegistry.register(group)
64 | routeState = .routing(current.appending(group))
65 | }
66 |
67 | public mutating func appendToPath(_ cmd: Command, ignoreName: Bool = false) {
68 | guard case let .routing(current) = routeState else {
69 | assertionFailure()
70 | return
71 | }
72 |
73 | optionRegistry.register(cmd)
74 | var commandPath = current.appending(cmd)
75 | commandPath.ignoreName = ignoreName
76 | routeState = .routed(commandPath, ParameterIterator(command: cmd))
77 | }
78 |
79 | }
80 |
81 | public var responders: [ParserResponse] = [AliasResponse(), OptionResponse(), RouteResponse(), ParameterResponse()]
82 | private var configuration = Configuration()
83 |
84 | public init() {}
85 |
86 | public subscript(dynamicMember keyPath: WritableKeyPath) -> U {
87 | get { configuration[keyPath: keyPath] }
88 | set { configuration[keyPath: keyPath] = newValue }
89 | }
90 |
91 | public func parse(cli: CLI, arguments: ArgumentList) throws -> CommandPath {
92 | var state = Parser.State(
93 | routeState: .routing(CommandGroupPath(top: cli)),
94 | optionRegistry: OptionRegistry(routable: cli),
95 | configuration: configuration
96 | )
97 |
98 | while arguments.hasNext() {
99 | if let responder = responders.first(where: { $0.canRespond(to: arguments, state: state) }) {
100 | state = try responder.respond(to: arguments, state: state)
101 | } else {
102 | preconditionFailure()
103 | }
104 | }
105 |
106 | try responders.forEach { (responder) in
107 | state = try responder.cleanUp(arguments: arguments, state: state )
108 | }
109 |
110 | if let command = state.command {
111 | return command
112 | } else {
113 | preconditionFailure()
114 | }
115 | }
116 |
117 | }
118 |
119 | // MARK: - ParserResponse
120 |
121 | public protocol ParserResponse {
122 | func canRespond(to arguments: ArgumentList, state: Parser.State) -> Bool
123 | func respond(to arguments: ArgumentList, state: Parser.State) throws -> Parser.State
124 | func cleanUp(arguments: ArgumentList, state: Parser.State) throws -> Parser.State
125 | }
126 |
127 | public struct AliasResponse: ParserResponse {
128 |
129 | public func canRespond(to arguments: ArgumentList, state: Parser.State) -> Bool {
130 | if case let .routing(groupPath) = state.routeState,
131 | groupPath.bottom.aliases[arguments.peek()] != nil {
132 | return true
133 | }
134 | return false
135 | }
136 |
137 | public func respond(to arguments: ArgumentList, state: Parser.State) throws -> Parser.State {
138 | guard case let .routing(groupPath) = state.routeState else {
139 | return state
140 | }
141 |
142 | arguments.manipulate { (args) in
143 | var copy = args
144 | if let alias = groupPath.bottom.aliases[copy[0]] {
145 | copy[0] = alias
146 | }
147 | return copy
148 | }
149 |
150 | return state
151 | }
152 |
153 | public func cleanUp(arguments: ArgumentList, state: Parser.State) throws -> Parser.State { state }
154 |
155 | }
156 |
157 | public struct OptionResponse: ParserResponse {
158 |
159 | public func canRespond(to arguments: ArgumentList, state: Parser.State) -> Bool {
160 | guard arguments.nextIsOption() else {
161 | return false
162 | }
163 |
164 | switch state.routeState {
165 | case .routing(_):
166 | return state.optionRegistry.recognizesOption(arguments.peek()) || state.configuration.fallback == nil
167 | case .routed(_, let params):
168 | return !params.nextIsCollection() || state.configuration.parseOptionsAfterCollectedParameter
169 | }
170 | }
171 |
172 | public func respond(to arguments: ArgumentList, state: Parser.State) throws -> Parser.State {
173 | let firstArg = arguments.pop()
174 |
175 | if firstArg.hasPrefix("--"), let equalsIndex = firstArg.firstIndex(of: "=") {
176 | let optName = String(firstArg[.. Parser.State {
229 | if let misused = state.optionRegistry.groups.first(where: { !$0.check() }) {
230 | throw OptionError(command: state.command, kind: .optionGroupMisuse(misused))
231 | }
232 | return state
233 | }
234 |
235 | }
236 |
237 | public struct RouteResponse: ParserResponse {
238 |
239 | public func canRespond(to arguments: ArgumentList, state: Parser.State) -> Bool {
240 | if case .routing = state.routeState {
241 | return true
242 | }
243 | return false
244 | }
245 |
246 | public func respond(to arguments: ArgumentList, state: Parser.State) throws -> Parser.State {
247 | guard case let .routing(groupPath) = state.routeState else {
248 | return state
249 | }
250 |
251 | var newState = state
252 |
253 | switch state.configuration.routeBehavior {
254 | case .automatically(let cmd):
255 | newState.appendToPath(cmd, ignoreName: true)
256 | case .search, .searchWithFallback(_):
257 | let name = arguments.peek()
258 |
259 | if let matching = groupPath.bottom.children.first(where: { $0.name == name }) {
260 | arguments.pop()
261 |
262 | if let group = matching as? CommandGroup {
263 | newState.appendToPath(group)
264 | } else if let cmd = matching as? Command {
265 | newState.appendToPath(cmd)
266 | } else {
267 | preconditionFailure("Routables must be either CommandGroups or Commands")
268 | }
269 | } else if let fallback = state.configuration.fallback {
270 | newState.appendToPath(fallback, ignoreName: true)
271 | } else {
272 | throw RouteError(partialPath: groupPath, notFound: name)
273 | }
274 | }
275 |
276 | return newState
277 | }
278 |
279 | public func cleanUp(arguments: ArgumentList, state: Parser.State) throws -> Parser.State {
280 | guard case let .routing(group) = state.routeState else {
281 | return state
282 | }
283 |
284 | var newState = state
285 |
286 | if let fallback = state.configuration.fallback {
287 | newState.appendToPath(fallback, ignoreName: true)
288 | } else if let command = group.bottom as? Command & CommandGroup {
289 | let commandPath = group.droppingLast().appending(command as Command)
290 | newState.routeState = .routed(commandPath, ParameterIterator(command: command))
291 | } else {
292 | throw RouteError(partialPath: group, notFound: nil)
293 | }
294 |
295 | return newState
296 | }
297 |
298 | }
299 |
300 | public struct ParameterResponse: ParserResponse {
301 |
302 | public func canRespond(to arguments: ArgumentList, state: Parser.State) -> Bool {
303 | if case .routed = state.routeState {
304 | return true
305 | }
306 | return false
307 | }
308 |
309 | public func respond(to arguments: ArgumentList, state: Parser.State) throws -> Parser.State {
310 | guard case let .routed(command, params) = state.routeState else {
311 | return state
312 | }
313 |
314 | if let namedParam = params.next() {
315 | let result = namedParam.param.update(to: arguments.pop())
316 | if case let .failure(error) = result {
317 | throw ParameterError(command: command, kind: .invalidValue(namedParam, error))
318 | }
319 | } else {
320 | throw ParameterError(command: command, kind: .wrongNumber(params.minCount, params.maxCount))
321 | }
322 |
323 | return state
324 | }
325 |
326 | public func cleanUp(arguments: ArgumentList, state: Parser.State) throws -> Parser.State {
327 | guard case let .routed(command, params) = state.routeState else {
328 | return state
329 | }
330 |
331 | if let namedParam = params.next(), !namedParam.param.satisfied {
332 | throw ParameterError(command: command, kind: .wrongNumber(params.minCount, params.maxCount))
333 | }
334 |
335 | return state
336 | }
337 |
338 | }
339 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Path.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Path.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 3/22/18.
6 | //
7 |
8 | public protocol RoutablePath: CustomStringConvertible {
9 | func joined(separator: String) -> String
10 | }
11 |
12 | extension RoutablePath {
13 | public var description: String {
14 | return "\(type(of: self))(\(joined(separator: " ")))"
15 | }
16 | }
17 |
18 | public struct CommandGroupPath: RoutablePath {
19 |
20 | public let groups: [CommandGroup]
21 |
22 | public var bottom: CommandGroup {
23 | return groups.last!
24 | }
25 |
26 | public init(top: CommandGroup, groups: [CommandGroup] = []) {
27 | self.init(groups: [top] + groups)
28 | }
29 |
30 | private init(groups: [CommandGroup]) {
31 | self.groups = groups
32 | }
33 |
34 | public func appending(_ group: CommandGroup) -> CommandGroupPath {
35 | return CommandGroupPath(groups: groups + [group])
36 | }
37 |
38 | public func appending(_ command: Command) -> CommandPath {
39 | return CommandPath(groupPath: self, command: command)
40 | }
41 |
42 | public func appending(_ routable: Routable) -> RoutablePath {
43 | if let cmd = routable as? Command {
44 | return CommandPath(groupPath: self, command: cmd)
45 | } else if let group = routable as? CommandGroup {
46 | return CommandGroupPath(groups: groups + [group])
47 | }
48 | fatalError()
49 | }
50 |
51 | public func droppingLast() -> CommandGroupPath {
52 | return CommandGroupPath(groups: Array(groups.dropLast()))
53 | }
54 |
55 | public func joined(separator: String = " ") -> String {
56 | return groups.map({ $0.name }).joined(separator: separator)
57 | }
58 |
59 | }
60 |
61 | public struct CommandPath: RoutablePath {
62 |
63 | public let groupPath: CommandGroupPath
64 | public let command: Command
65 | public var ignoreName = false
66 |
67 | public var options: [Option] {
68 | return command.options + groupPath.groups.map({ $0.options }).joined()
69 | }
70 |
71 | public var usage: String {
72 | var message = joined()
73 |
74 | if !command.parameters.isEmpty {
75 | let signature = command.parameters.map({ $0.signature }).joined(separator: " ")
76 | message += " \(signature)"
77 | }
78 |
79 | if !options.isEmpty {
80 | message += " [options]"
81 | }
82 |
83 | return message
84 | }
85 |
86 | public init(groupPath: CommandGroupPath, command: Command) {
87 | self.groupPath = groupPath
88 | self.command = command
89 | }
90 |
91 | public func joined(separator: String = " ") -> String {
92 | var str = groupPath.joined(separator: separator)
93 | if !ignoreName {
94 | str += separator + command.name
95 | }
96 | return str
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Term.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Term.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 9/14/17.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum Term {
11 |
12 | public static let isTTY = isatty(fileno(Foundation.stdout)) != 0
13 |
14 | public static var stdout: WritableStream = WriteStream.stdout
15 | public static var stderr: WritableStream = WriteStream.stderr
16 | public static var stdin: ReadableStream = ReadStream.stdin
17 | public static var read: () -> String? = { readLine() }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/Validation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Validation.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 11/21/18.
6 | //
7 |
8 | public protocol AnyValidation {
9 | var message: String { get }
10 | }
11 |
12 | public struct Validation: AnyValidation {
13 |
14 | public enum Result {
15 | case success
16 | case failure(String)
17 | }
18 |
19 | public typealias ValidatorBlock = (T) -> Bool
20 |
21 | public static func custom(_ message: String, _ validator: @escaping ValidatorBlock) -> Validation {
22 | return .init(message, validator)
23 | }
24 |
25 | public let block: ValidatorBlock
26 | public let message: String
27 |
28 | init(_ message: String, _ block: @escaping ValidatorBlock) {
29 | self.block = block
30 | self.message = message
31 | }
32 |
33 | public func validate(_ value: T) -> Bool {
34 | guard block(value) else {
35 | return false
36 | }
37 | return true
38 | }
39 |
40 | }
41 |
42 | extension Validation where T: Equatable {
43 |
44 | public static func allowing(_ values: T..., message: String? = nil) -> Validation {
45 | let commaSeparated = values.map({ String(describing: $0) }).joined(separator: ", ")
46 | return .init(message ?? "must be one of: \(commaSeparated)") { values.contains($0) }
47 | }
48 |
49 | public static func rejecting(_ values: T..., message: String? = nil) -> Validation {
50 | let commaSeparated = values.map({ String(describing: $0) }).joined(separator: ", ")
51 | return .init(message ?? "must not be: \(commaSeparated)") { !values.contains($0) }
52 | }
53 |
54 | }
55 |
56 | extension Validation where T: Comparable {
57 |
58 | public static func greaterThan(_ value: T, message: String? = nil) -> Validation {
59 | return .init(message ?? "must be greater than \(value)") { $0 > value }
60 | }
61 |
62 | public static func lessThan(_ value: T, message: String? = nil) -> Validation {
63 | return .init(message ?? "must be less than \(value)") { $0 < value }
64 | }
65 |
66 | public static func within(_ range: ClosedRange, message: String? = nil) -> Validation {
67 | return .init(message ?? "must be greater than or equal to \(range.lowerBound) and less than or equal to \(range.upperBound)") { range.contains($0) }
68 | }
69 |
70 | public static func within(_ range: Range, message: String? = nil) -> Validation {
71 | return .init(message ?? "must be greater than or equal to \(range.lowerBound) and less than \(range.upperBound)") { range.contains($0) }
72 | }
73 |
74 | }
75 |
76 | extension Validation where T == String {
77 |
78 | public static func contains(_ substring: String, message: String? = nil) -> Validation {
79 | return .init(message ?? "must contain '\(substring)'") { $0.contains(substring) }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/ValueBox.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValueBox.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 12/27/18.
6 | //
7 |
8 | public enum UpdateResult {
9 | case success
10 | case failure(InvalidValueReason)
11 | }
12 |
13 | public enum InvalidValueReason {
14 | case conversionError
15 | case validationError(AnyValidation)
16 | }
17 |
18 | // MARK: -
19 |
20 | public protocol AnyValueBox: AnyObject {
21 | var completion: ShellCompletion { get }
22 | var valueType: ConvertibleFromString.Type { get }
23 |
24 | func update(to value: String) -> UpdateResult
25 | }
26 |
27 | public protocol ValueBox: AnyValueBox {
28 | associatedtype Value: ConvertibleFromString
29 |
30 | var validation: [Validation] { get }
31 |
32 | func update(to value: Value)
33 | }
34 |
35 | extension ValueBox {
36 |
37 | public var valueType: ConvertibleFromString.Type { return Value.self }
38 |
39 | public func update(to value: String) -> UpdateResult {
40 | guard let converted = Value(input: value) else {
41 | return .failure(.conversionError)
42 | }
43 |
44 | if let failedValidation = validation.first(where: { $0.validate(converted) == false }) {
45 | return .failure(.validationError(failedValidation))
46 | }
47 |
48 | update(to: converted)
49 |
50 | return .success
51 | }
52 |
53 | }
54 |
55 | // MARK: - ConvertibleFromString
56 |
57 | /// A type that can be created from a string
58 | public protocol ConvertibleFromString {
59 |
60 | /// Returns an instance of the conforming type from a string representation
61 | init?(input: String)
62 |
63 | static var explanationForConversionFailure: String { get }
64 |
65 | static func messageForInvalidValue(reason: InvalidValueReason, for id: String?) -> String
66 | }
67 |
68 | extension ConvertibleFromString {
69 | public static var explanationForConversionFailure: String {
70 | return "expected \(self)"
71 | }
72 |
73 | public static func messageForInvalidValue(reason: InvalidValueReason, for id: String?) -> String {
74 | var message = "invalid value"
75 | if let id = id {
76 | message += " passed to '\(id)'"
77 | }
78 |
79 | message += "; "
80 |
81 | switch reason {
82 | case .conversionError: message += explanationForConversionFailure
83 | case let .validationError(validation): message += validation.message
84 | }
85 |
86 | return message
87 | }
88 |
89 | }
90 |
91 | extension CaseIterable where Self: ConvertibleFromString {
92 | public static var explanationForConversionFailure: String {
93 | let options = allCases.map({ String(describing: $0) }).joined(separator: ", ")
94 | return "expected one of: \(options)"
95 | }
96 | }
97 |
98 | extension LosslessStringConvertible where Self: ConvertibleFromString {
99 | public init?(input: String) {
100 | guard let val = Self(input) else {
101 | return nil
102 | }
103 | self = val
104 | }
105 | }
106 |
107 | extension RawRepresentable where Self: ConvertibleFromString, Self.RawValue: ConvertibleFromString {
108 | public init?(input: String) {
109 | guard let raw = RawValue(input: input), let val = Self(rawValue: raw) else {
110 | return nil
111 | }
112 | self = val
113 | }
114 | }
115 |
116 | extension Optional: ConvertibleFromString where Wrapped: ConvertibleFromString {
117 |
118 | public static var explanationForConversionFailure: String {
119 | return Wrapped.explanationForConversionFailure
120 | }
121 |
122 | public static func messageForInvalidValue(reason: InvalidValueReason, for id: String?) -> String {
123 | return Wrapped.messageForInvalidValue(reason: reason, for: id)
124 | }
125 |
126 | public init?(input: String) {
127 | if let wrapped = Wrapped(input: input) {
128 | self = .some(wrapped)
129 | } else {
130 | return nil
131 | }
132 | }
133 | }
134 |
135 | public protocol OptionType {
136 | associatedtype Wrapped
137 | static var swiftcli_Empty: Self { get }
138 |
139 | var swiftcli_Value: Wrapped? { get }
140 | }
141 | extension Optional: OptionType {
142 | public static var swiftcli_Empty: Self { .none }
143 |
144 | public var swiftcli_Value: Wrapped? {
145 | if case .some(let value) = self {
146 | return value
147 | }
148 | return nil
149 | }
150 | }
151 |
152 | extension String: ConvertibleFromString {}
153 | extension Int: ConvertibleFromString {}
154 | extension Float: ConvertibleFromString {}
155 | extension Double: ConvertibleFromString {}
156 |
157 | extension Bool: ConvertibleFromString {
158 |
159 | /// Returns a bool from a string representation
160 | ///
161 | /// - parameter input: A string representation of a bool value
162 | ///
163 | /// This is case insensitive and recognizes several representations:
164 | ///
165 | /// - true/false
166 | /// - t/f
167 | /// - yes/no
168 | /// - y/n
169 | public init?(input: String) {
170 | let lowercased = input.lowercased()
171 |
172 | if ["y", "yes", "t", "true"].contains(lowercased) {
173 | self = true
174 | } else if ["n", "no", "f", "false"].contains(lowercased) {
175 | self = false
176 | } else {
177 | return nil
178 | }
179 | }
180 |
181 | }
182 |
--------------------------------------------------------------------------------
/Sources/SwiftCLI/VersionCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VersionCommand.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 8/2/14.
6 | // Copyright (c) 2014 jakeheis. All rights reserved.
7 | //
8 |
9 | public class VersionCommand: Command {
10 |
11 | public let name = "version"
12 | public let shortDescription = "Prints the current version of this app"
13 |
14 | let version: String
15 |
16 | init(version: String) {
17 | self.version = version
18 | }
19 |
20 | public func execute() throws {
21 | stdout <<< "Version: \(version)"
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftCLI.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "SwiftCLI"
4 | s.version = "6.0.0"
5 | s.summary = "A powerful framework that can be used to develop a CLI in Swift"
6 |
7 | s.description = <<-DESC
8 | A powerful framework that can be used to develop a CLI, from the simplest to the most complex, in Swift.
9 | DESC
10 |
11 | s.homepage = "https://github.com/jakeheis/SwiftCLI"
12 | s.license = "MIT"
13 | s.author = { "Jake Heiser" => "email@address.com" }
14 | s.source = { :git => "https://github.com/jakeheis/SwiftCLI.git", :tag => "#{s.version}" }
15 |
16 | s.platform = :osx
17 | s.osx.deployment_target = "10.9"
18 |
19 | s.source_files = "Sources", "Sources/**/*.{swift}"
20 |
21 | s.swift_version = "5.1.1"
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/Tests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | ${EXECUTABLE_NAME}
9 | CFBundleIdentifier
10 | jakeheis.${PRODUCT_NAME:rfc1034identifier}
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | ${PRODUCT_NAME}
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SwiftCLITests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += SwiftCLITests.__allTests()
7 |
8 | XCTMain(tests)
9 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/ArgumentListTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArgumentListTests.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 3/29/17.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SwiftCLI
11 |
12 | class ArgumentListTests: XCTestCase {
13 |
14 | func testManipulate() {
15 | let args = ArgumentList(arguments: ["tester", "test", "thisCase"])
16 | args.manipulate { (args) in
17 | return args.map { $0.uppercased() } + ["last"]
18 | }
19 |
20 | XCTAssertEqual(args.pop(), "TESTER")
21 | XCTAssertEqual(args.pop(), "TEST")
22 | XCTAssertEqual(args.pop(), "THISCASE")
23 | XCTAssertEqual(args.pop(), "last")
24 | XCTAssertFalse(args.hasNext())
25 | }
26 |
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/CompletionGeneratorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompletionGeneratorTests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 9/10/17.
6 | //
7 |
8 | import XCTest
9 | @testable import SwiftCLI
10 |
11 | class CompletionGeneratorTests: XCTestCase {
12 |
13 | func testGroup() {
14 | let cli = CLI.createTester(commands: [alphaCmd, betaCmd])
15 |
16 | let generator = ZshCompletionGenerator(cli: cli)
17 | let capture = CaptureStream()
18 | generator.writeGroup(for: CommandGroupPath(top: cli), into: capture)
19 | capture.closeWrite()
20 |
21 | XCTAssertEqual(capture.readAll(), """
22 | _tester() {
23 | _arguments -C \\
24 | '(-h --help)'{-h,--help}"[Show help information]" \\
25 | '(-): :->command' \\
26 | '(-)*:: :->arg' && return
27 | case $state in
28 | (command)
29 | local commands
30 | commands=(
31 | "alpha:The alpha command"
32 | "beta:A beta command"
33 | "help:Prints help information"
34 | )
35 | _describe 'command' commands
36 | ;;
37 | (arg)
38 | case ${words[1]} in
39 | (alpha)
40 | _tester_alpha
41 | ;;
42 | (beta)
43 | _tester_beta
44 | ;;
45 | (help)
46 | _tester_help
47 | ;;
48 | esac
49 | ;;
50 | esac
51 | }
52 | _tester_alpha() {
53 | _arguments -C
54 | }
55 | _tester_beta() {
56 | _arguments -C
57 | }
58 | _tester_help() {
59 | _arguments -C \\
60 | "*::command: "
61 | }
62 |
63 | """)
64 | }
65 |
66 | func testBasicOptions() {
67 | let cmd = DoubleFlagCmd()
68 | let cli = CLI.createTester(commands: [cmd])
69 |
70 | let generator = ZshCompletionGenerator(cli: cli)
71 | let capture = CaptureStream()
72 | let path = CommandGroupPath(top: cli).appending(cmd)
73 | generator.writeCommand(for: path, into: capture)
74 | capture.closeWrite()
75 |
76 | XCTAssertEqual(capture.readAll(), """
77 | _tester_cmd() {
78 | _arguments -C \\
79 | '(-a --alpha)'{-a,--alpha}"[The alpha flag]" \\
80 | '(-b --beta)'{-b,--beta}"[The beta flag]"
81 | }
82 |
83 | """)
84 | }
85 |
86 | func testSepcialCaseOptionCompletion() {
87 | let variadicKey = VariadicKeyCmd()
88 | let exactlyOne = ExactlyOneCmd()
89 | let counterFlag = CounterFlagCmd()
90 |
91 | let cli = CLI.createTester(commands: [variadicKey, exactlyOne, counterFlag])
92 | let generator = ZshCompletionGenerator(cli: cli)
93 |
94 | let variadicKeyCapture = CaptureStream()
95 | generator.writeCommand(for: CommandGroupPath(top: cli).appending(variadicKey), into: variadicKeyCapture)
96 | variadicKeyCapture.closeWrite()
97 | XCTAssertEqual(variadicKeyCapture.readAll(), """
98 | _tester_cmd() {
99 | _arguments -C \\
100 | "*-f[a file]: :_files" \\
101 | "*--file[a file]: :_files"
102 | }
103 |
104 | """)
105 |
106 | let exactlyOneCapture = CaptureStream()
107 | generator.writeCommand(for: CommandGroupPath(top: cli).appending(exactlyOne), into: exactlyOneCapture)
108 | exactlyOneCapture.closeWrite()
109 | XCTAssertEqual(exactlyOneCapture.readAll(), """
110 | _tester_cmd() {
111 | _arguments -C \\
112 | '(-a --alpha -b --beta)'{-a,--alpha}"[the alpha flag]" \\
113 | '(-a --alpha -b --beta)'{-b,--beta}"[the beta flag]"
114 | }
115 |
116 | """)
117 |
118 | let counterFlagCapture = CaptureStream()
119 | generator.writeCommand(for: CommandGroupPath(top: cli).appending(counterFlag), into: counterFlagCapture)
120 | counterFlagCapture.closeWrite()
121 | XCTAssertEqual(counterFlagCapture.readAll(), """
122 | _tester_cmd() {
123 | _arguments -C \\
124 | "*-v[Increase the verbosity]" \\
125 | "*--verbose[Increase the verbosity]"
126 | }
127 |
128 | """)
129 | }
130 |
131 | func testParameterCompletion() {
132 | let req2 = Req2Cmd()
133 | let req2Collected = Req2CollectedCmd()
134 | let req2opt2 = Req2Opt2Cmd()
135 |
136 | let cli = CLI.createTester(commands: [req2, req2Collected, req2opt2])
137 | let generator = ZshCompletionGenerator(cli: cli)
138 |
139 | let req2Capture = CaptureStream()
140 | generator.writeCommand(for: CommandGroupPath(top: cli).appending(req2), into: req2Capture)
141 | req2Capture.closeWrite()
142 |
143 | XCTAssertEqual(req2Capture.readAll(), """
144 | _tester_cmd() {
145 | _arguments -C \\
146 | ":req1:_files" \\
147 | ":req2:{_values '' 'executable[generates a project for a cli executable]' 'library[generates project for a dynamic library]' 'other'}"
148 | }
149 |
150 | """)
151 |
152 | let req2CollectedCapture = CaptureStream()
153 | generator.writeCommand(for: CommandGroupPath(top: cli).appending(req2Collected), into: req2CollectedCapture)
154 | req2CollectedCapture.closeWrite()
155 |
156 | XCTAssertEqual(req2CollectedCapture.readAll(), """
157 | _tester_cmd() {
158 | _arguments -C \\
159 | ":req1:{_values '' 'executable[generates a project for a cli executable]' 'library[generates project for a dynamic library]'}" \\
160 | "*:req2:_files"
161 | }
162 |
163 | """)
164 |
165 | let req2opt2Capture = CaptureStream()
166 | generator.writeCommand(for: CommandGroupPath(top: cli).appending(req2opt2), into: req2opt2Capture)
167 | req2opt2Capture.closeWrite()
168 |
169 | XCTAssertEqual(req2opt2Capture.readAll(), """
170 | _tester_cmd() {
171 | _arguments -C \\
172 | ":req1:_files" \\
173 | ":req2:_swift_dependency" \\
174 | "::opt1: " \\
175 | "::opt2:_files"
176 | }
177 |
178 | """)
179 | }
180 |
181 | func testLayered() {
182 | let cli = CLI.createTester(commands: [alphaCmd, betaCmd, intraGroup])
183 |
184 | let generator = ZshCompletionGenerator(cli: cli)
185 | let capture = CaptureStream()
186 | generator.writeCompletions(into: capture)
187 | capture.closeWrite()
188 |
189 | XCTAssertEqual(capture.readAll(), """
190 | #compdef tester
191 | local context state state_descr line
192 | typeset -A opt_args
193 |
194 | _tester() {
195 | _arguments -C \\
196 | '(-h --help)'{-h,--help}"[Show help information]" \\
197 | '(-): :->command' \\
198 | '(-)*:: :->arg' && return
199 | case $state in
200 | (command)
201 | local commands
202 | commands=(
203 | "alpha:The alpha command"
204 | "beta:A beta command"
205 | "intra:The intra level of commands"
206 | "help:Prints help information"
207 | )
208 | _describe 'command' commands
209 | ;;
210 | (arg)
211 | case ${words[1]} in
212 | (alpha)
213 | _tester_alpha
214 | ;;
215 | (beta)
216 | _tester_beta
217 | ;;
218 | (intra)
219 | _tester_intra
220 | ;;
221 | (help)
222 | _tester_help
223 | ;;
224 | esac
225 | ;;
226 | esac
227 | }
228 | _tester_alpha() {
229 | _arguments -C
230 | }
231 | _tester_beta() {
232 | _arguments -C
233 | }
234 | _tester_intra() {
235 | _arguments -C \\
236 | '(-): :->command' \\
237 | '(-)*:: :->arg' && return
238 | case $state in
239 | (command)
240 | local commands
241 | commands=(
242 | "charlie:A beta command"
243 | "delta:A beta command"
244 | )
245 | _describe 'command' commands
246 | ;;
247 | (arg)
248 | case ${words[1]} in
249 | (charlie)
250 | _tester_intra_charlie
251 | ;;
252 | (delta)
253 | _tester_intra_delta
254 | ;;
255 | esac
256 | ;;
257 | esac
258 | }
259 | _tester_intra_charlie() {
260 | _arguments -C
261 | }
262 | _tester_intra_delta() {
263 | _arguments -C
264 | }
265 | _tester_help() {
266 | _arguments -C \\
267 | "*::command: "
268 | }
269 | _tester
270 |
271 | """)
272 | }
273 |
274 | func testEscaping() {
275 | let cmd = QuoteDesciptionCmd()
276 |
277 | let cli = CLI.createTester(commands: [cmd])
278 | let generator = ZshCompletionGenerator(cli: cli)
279 |
280 | let capture = CaptureStream()
281 | generator.writeGroup(for: CommandGroupPath(top: cli), into: capture)
282 | capture.closeWrite()
283 |
284 | XCTAssertEqual(capture.readAll(), """
285 | _tester() {
286 | _arguments -C \\
287 | '(-h --help)'{-h,--help}"[Show help information]" \\
288 | '(-): :->command' \\
289 | '(-)*:: :->arg' && return
290 | case $state in
291 | (command)
292 | local commands
293 | commands=(
294 | "cmd:this description has a \\"quoted section\\""
295 | "help:Prints help information"
296 | )
297 | _describe 'command' commands
298 | ;;
299 | (arg)
300 | case ${words[1]} in
301 | (cmd)
302 | _tester_cmd
303 | ;;
304 | (help)
305 | _tester_help
306 | ;;
307 | esac
308 | ;;
309 | esac
310 | }
311 | _tester_cmd() {
312 | _arguments -C \\
313 | '(-q --quoted)'{-q,--quoted}"[also has \\"quotes\\"]"
314 | }
315 | _tester_help() {
316 | _arguments -C \\
317 | "*::command: "
318 | }
319 |
320 | """)
321 | }
322 |
323 | func testFunction() {
324 | let body = """
325 | echo wassup
326 | """
327 |
328 | let cmd = Req1Cmd()
329 |
330 | let cli = CLI.createTester(commands: [cmd])
331 | let generator = ZshCompletionGenerator(cli: cli, functions: [
332 | "_ice_targets": body
333 | ])
334 |
335 | let justFunctionCapture = CaptureStream()
336 | generator.writeFunction(name: "_ice_targets", body: body, into: justFunctionCapture)
337 | justFunctionCapture.closeWrite()
338 |
339 | XCTAssertEqual(justFunctionCapture.readAll(), """
340 | _ice_targets() {
341 | echo wassup
342 | }
343 |
344 | """)
345 |
346 | let fullCapture = CaptureStream()
347 | generator.writeCompletions(into: fullCapture)
348 | fullCapture.closeWrite()
349 |
350 | XCTAssertEqual(fullCapture.readAll(), """
351 | #compdef tester
352 | local context state state_descr line
353 | typeset -A opt_args
354 |
355 | _tester() {
356 | _arguments -C \\
357 | '(-h --help)'{-h,--help}"[Show help information]" \\
358 | '(-): :->command' \\
359 | '(-)*:: :->arg' && return
360 | case $state in
361 | (command)
362 | local commands
363 | commands=(
364 | "cmd:"
365 | "help:Prints help information"
366 | )
367 | _describe 'command' commands
368 | ;;
369 | (arg)
370 | case ${words[1]} in
371 | (cmd)
372 | _tester_cmd
373 | ;;
374 | (help)
375 | _tester_help
376 | ;;
377 | esac
378 | ;;
379 | esac
380 | }
381 | _tester_cmd() {
382 | _arguments -C \\
383 | ":req1:_ice_targets"
384 | }
385 | _tester_help() {
386 | _arguments -C \\
387 | "*::command: "
388 | }
389 | _ice_targets() {
390 | echo wassup
391 | }
392 | _tester
393 |
394 | """)
395 | }
396 |
397 | func testOptionCompletion() {
398 | let cmd = CompletionOptionCmd()
399 |
400 | let cli = CLI.createTester(commands: [cmd])
401 | let generator = ZshCompletionGenerator(cli: cli)
402 |
403 | let capture = CaptureStream()
404 | generator.writeCommand(for: CommandGroupPath(top: cli).appending(cmd), into: capture)
405 | capture.closeWrite()
406 |
407 | XCTAssertEqual(capture.readAll(), """
408 | _tester_cmd() {
409 | _arguments -C \\
410 | '(-v --values)'{-v,--values}"[]: :{_values '' 'opt1[first option]' 'opt2[second option]'}" \\
411 | '(-f --function)'{-f,--function}"[]: :_a_func" \\
412 | '(-n --name)'{-n,--name}"[]: :_files" \\
413 | '(-z --zero)'{-z,--zero}"[]: : " \\
414 | '(-d --default)'{-d,--default}"[]: :_files" \\
415 | '(-f --flag)'{-f,--flag}"[]"
416 | }
417 |
418 | """)
419 | }
420 |
421 | }
422 |
423 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/Fixtures.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftCLITests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 6/28/16.
6 | // Copyright (c) 2016 jakeheis. All rights reserved.
7 | //
8 |
9 | import SwiftCLI
10 | import XCTest
11 |
12 | extension CLI {
13 | static func createTester(commands: [Routable], description: String? = nil) -> CLI {
14 | return CLI(name: "tester", description: description, commands: commands)
15 | }
16 | }
17 |
18 | class TestCommand: Command {
19 |
20 | let name = "test"
21 | let shortDescription = "A command to test stuff"
22 |
23 | var executionString = ""
24 |
25 | @Param var testName: String
26 | @Param var testerName: String?
27 |
28 | @Flag("-s", "--silent", description: "Silence all test output")
29 | var silent: Bool
30 |
31 | @Key("-t", "--times", description: "Number of times to run the test")
32 | var times: Int?
33 |
34 | let completion: ((_ executionString: String) -> ())?
35 |
36 | init(completion: ((_ executionString: String) -> ())? = nil) {
37 | self.completion = completion
38 | }
39 |
40 | func execute() throws {
41 | executionString = "\(testerName ?? "defaultTester") will test \(testName), \(times ?? 1) times"
42 | if silent {
43 | executionString += ", silently"
44 | }
45 |
46 | completion?(executionString)
47 | }
48 |
49 | }
50 |
51 | class TestCommandWithLongDescription: Command {
52 | let name = "test"
53 | let shortDescription = "A command to test stuff"
54 | let longDescription = "This is a long\nmultiline description"
55 |
56 | func execute() throws {}
57 | }
58 |
59 | class MultilineCommand: Command {
60 |
61 | let name = "test"
62 | let shortDescription = "A command that has multiline comments.\nNew line"
63 |
64 | @Flag("-s", "--silent", description: "Silence all test output\nNewline")
65 | var silent: Bool
66 |
67 | @Key("-t", "--times", description: "Number of times to run the test")
68 | var times: Int?
69 |
70 | func execute() throws {}
71 |
72 | }
73 |
74 | class ReversedOrderCommand: Command {
75 |
76 | let name = "test"
77 | let shortDescription = "A command"
78 |
79 | @Flag("--silent", "-s", description: "Silence all test output\nNewline")
80 | var silent: Bool
81 |
82 | @Key("--times", "-t", description: "Number of times to run the test")
83 | var times: Int?
84 |
85 | func execute() throws {}
86 |
87 | }
88 |
89 | class TestInheritedCommand: TestCommand {
90 | @Flag("-v", "--verbose", description: "Show more output information")
91 | var verbose: Bool
92 | }
93 |
94 | // MARK: -
95 |
96 | let alphaCmd = AlphaCmd()
97 | let betaCmd = BetaCmd()
98 | let charlieCmd = CharlieCmd()
99 | let deltaCmd = DeltaCmd()
100 |
101 | class AlphaCmd: Command {
102 | let name = "alpha"
103 | let shortDescription = "The alpha command"
104 | fileprivate init() {}
105 | func execute() throws {}
106 | }
107 |
108 | class BetaCmd: Command {
109 | let name = "beta"
110 | let shortDescription = "A beta command"
111 | fileprivate init() {}
112 | func execute() throws {}
113 | }
114 |
115 | class CharlieCmd: Command {
116 | let name = "charlie"
117 | let shortDescription = "A beta command"
118 | fileprivate init() {}
119 | func execute() throws {}
120 | }
121 |
122 | class DeltaCmd: Command {
123 | let name = "delta"
124 | let shortDescription = "A beta command"
125 | fileprivate init() {}
126 | func execute() throws {}
127 | }
128 |
129 | class EmptyCmd: Command {
130 | let name = "cmd"
131 | func execute() throws {}
132 | }
133 |
134 | class Req1Cmd: EmptyCmd {
135 | @Param(completion: .function("_ice_targets"))
136 | var req1: String
137 | }
138 |
139 | class Opt1Cmd: EmptyCmd {
140 | @Param var opt1: String?
141 | }
142 |
143 | class Req2Cmd: EmptyCmd {
144 | @Param(completion: .filename)
145 | var req1: String
146 |
147 | @Param(completion: .values([
148 | ("executable", "generates a project for a cli executable"),
149 | ("library", "generates project for a dynamic library"),
150 | ("other", "")
151 | ]))
152 | var req2: String
153 | }
154 |
155 | class Opt2Cmd: EmptyCmd {
156 | @Param(completion: .filename)
157 | var opt1: String?
158 |
159 | @Param(completion: .values([
160 | ("executable", "generates a project for a cli executable"),
161 | ("library", "generates project for a dynamic library")
162 | ]))
163 | var opt2: String?
164 | }
165 |
166 | class Opt2InhCmd: Opt2Cmd {
167 | @Param var opt3: String?
168 | }
169 |
170 | class ReqCollectedCmd: EmptyCmd {
171 | @CollectedParam(minCount: 1) var req1: [String]
172 | }
173 |
174 | class OptCollectedCmd: EmptyCmd {
175 | @CollectedParam var opt1: [String]
176 | }
177 |
178 | class Req2CollectedCmd: EmptyCmd {
179 | @Param(completion: .values([
180 | ("executable", "generates a project for a cli executable"),
181 | ("library", "generates project for a dynamic library")
182 | ]))
183 | var req1: String
184 |
185 | @CollectedParam(minCount: 1, completion: .filename)
186 | var req2: [String]
187 | }
188 |
189 | class TriReqCollectedCmd: EmptyCmd {
190 | @CollectedParam(minCount: 3)
191 | var triReq: [String]
192 | }
193 |
194 | class Opt2CollectedCmd: EmptyCmd {
195 | @Param var opt1: String?
196 | @CollectedParam var opt2: [String]
197 | }
198 |
199 | class Req2Opt2Cmd: EmptyCmd {
200 | @Param(completion: .filename)
201 | var req1: String
202 |
203 | @Param(completion: .function("_swift_dependency"))
204 | var req2: String
205 |
206 | @Param(completion: .none)
207 | var opt1: String?
208 |
209 | @Param(completion: .filename)
210 | var opt2: String?
211 | }
212 |
213 | // MARK: -
214 |
215 | let midGroup = MidGroup()
216 | let intraGroup = IntraGroup()
217 |
218 | class MidGroup: CommandGroup {
219 | let name = "mid"
220 | let shortDescription = "The mid level of commands"
221 | let children: [Routable] = [alphaCmd, betaCmd]
222 | fileprivate init() {}
223 | }
224 |
225 | class IntraGroup: CommandGroup {
226 | let name = "intra"
227 | let shortDescription = "The intra level of commands"
228 | let children: [Routable] = [charlieCmd, deltaCmd]
229 | fileprivate init() {}
230 | }
231 |
232 | // MARK: -
233 |
234 | class OptionCmd: Command {
235 | let name = "cmd"
236 | let shortDescription = ""
237 | func execute() throws {}
238 | }
239 |
240 | class FlagCmd: OptionCmd {
241 | @Flag("-a", "--alpha")
242 | var flag: Bool
243 | }
244 |
245 | class KeyCmd: OptionCmd {
246 | @Key("-a", "--alpha")
247 | var key: String?
248 | }
249 |
250 | class DoubleFlagCmd: OptionCmd {
251 | @Flag("-a", "--alpha", description: "The alpha flag")
252 | var alpha: Bool
253 |
254 | @Flag("-b", "--beta", description: "The beta flag")
255 | var beta: Bool
256 | }
257 |
258 | class DoubleKeyCmd: OptionCmd {
259 | @Key("-a", "--alpha")
260 | var alpha: String?
261 |
262 | @Key("-b", "--beta")
263 | var beta: String?
264 | }
265 |
266 | class FlagKeyCmd: OptionCmd {
267 | @Flag("-a", "--alpha")
268 | var alpha: Bool
269 |
270 | @Key("-b", "--beta")
271 | var beta: String?
272 | }
273 |
274 | class FlagKeyParamCmd: OptionCmd {
275 | @Flag("-a", "--alpha")
276 | var alpha: Bool
277 |
278 | @Key("-b", "--beta")
279 | var beta: String?
280 |
281 | @Param var param: String
282 | }
283 |
284 | class IntKeyCmd: OptionCmd {
285 | @Key("-a", "--alpha")
286 | var alpha: Int?
287 | }
288 |
289 | class ExactlyOneCmd: Command {
290 | let name = "cmd"
291 | let shortDescription = ""
292 | var helpFlag: Flag? = nil
293 | func execute() throws {}
294 |
295 | @Flag("-a", "--alpha", description: "the alpha flag")
296 | var alpha: Bool
297 |
298 | @Flag("-b", "--beta", description: "the beta flag")
299 | var beta: Bool
300 |
301 | lazy var optionGroups: [OptionGroup] = [.exactlyOne($alpha, $beta)]
302 | }
303 |
304 | class MultipleRestrictionsCmd: Command {
305 | let name = "cmd"
306 |
307 | @Flag("-a", "--alpha", description: "the alpha flag")
308 | var alpha: Bool
309 |
310 | @Flag("-b", "--beta", description: "the beta flag")
311 | var beta: Bool
312 |
313 | lazy var atMostOne: OptionGroup = .atMostOne($alpha, $beta)
314 | lazy var atMostOneAgain: OptionGroup = .atMostOne($alpha, $beta)
315 |
316 | var optionGroups: [OptionGroup] {
317 | return [atMostOne, atMostOneAgain]
318 | }
319 |
320 | func execute() throws {}
321 | }
322 |
323 | class VariadicKeyCmd: OptionCmd {
324 | @VariadicKey("-f", "--file", description: "a file")
325 | var files: [String]
326 | }
327 |
328 | class CounterFlagCmd: OptionCmd {
329 | @CounterFlag("-v", "--verbose", description: "Increase the verbosity")
330 | var verbosity: Int
331 | }
332 |
333 | class ValidatedKeyCmd: OptionCmd {
334 |
335 | static let capitalizedFirstName = Validation.custom("Must be a capitalized first name") { $0.capitalized == $0 }
336 |
337 | @Key("-n", "--name", validation: [capitalizedFirstName])
338 | var firstName: String?
339 |
340 | @Key("-a", "--age", validation: [.greaterThan(18)])
341 | var age: Int?
342 |
343 | @Key("-l", "--location", validation: [.rejecting("Chicago", "Boston")])
344 | var location: String?
345 |
346 | @Key("--holiday", validation: [.allowing("Thanksgiving", "Halloween")])
347 | var holiday: String?
348 |
349 | }
350 |
351 | class QuoteDesciptionCmd: Command {
352 | let name = "cmd"
353 | let shortDescription = "this description has a \"quoted section\""
354 |
355 | @Flag("-q", "--quoted", description: "also has \"quotes\"")
356 | var flag: Bool
357 |
358 | func execute() throws {}
359 | }
360 |
361 | class CompletionOptionCmd: OptionCmd {
362 | @Key("-v", "--values", completion: .values([("opt1", "first option"), ("opt2", "second option")]))
363 | var values: String?
364 |
365 | @Key("-f", "--function", completion: .function("_a_func"))
366 | var function: String?
367 |
368 | @Key("-n", "--name", completion: .filename)
369 | var filename: String?
370 |
371 | @Key("-z", "--zero", completion: .none)
372 | var none: String?
373 |
374 | @Key("-d", "--default")
375 | var def: String?
376 |
377 | @Flag("-f", "--flag")
378 | var flag: Bool
379 | }
380 |
381 | class EnumCmd: Command {
382 |
383 | enum Speed: String, ConvertibleFromString {
384 | case slow
385 | case fast
386 | }
387 |
388 | enum Single: String, ConvertibleFromString {
389 | case value
390 |
391 | static let explanationForConversionFailure = "only can be 'value'"
392 | }
393 |
394 | let name = "cmd"
395 | let shortDescription = "Limits param values to enum"
396 |
397 | @Param var speed: Speed
398 | @Param var single: Single?
399 | @Param var int: Int?
400 |
401 | func execute() throws {}
402 |
403 | }
404 |
405 | #if swift(>=4.1.50)
406 | extension EnumCmd.Speed: CaseIterable {}
407 | #endif
408 |
409 | class ValidatedParamCmd: Command {
410 |
411 | let name = "cmd"
412 | let shortDescription = "Validates param values"
413 |
414 | @Param(validation: .greaterThan(18))
415 | var age: Int?
416 |
417 | func execute() throws {}
418 |
419 | }
420 |
421 | class RememberExecutionCmd: Command {
422 |
423 | let name = "cmd"
424 | let shortDescription = "Remembers execution"
425 |
426 | @Param var param: String?
427 |
428 | var executed = false
429 |
430 | func execute() throws {
431 | executed = true
432 | }
433 |
434 | }
435 |
436 | class ParamInitCmd: Command {
437 | let name = "cmd"
438 |
439 | @Param(completion: .filename)
440 | var reqComp: String
441 |
442 | @Param(completion: .filename)
443 | var optComp: String?
444 |
445 | @Param(validation: .allowing("hi"))
446 | var reqVal: String
447 |
448 | @Param(validation: .allowing("yo"))
449 | var optVal: String?
450 |
451 | @Param(completion: .filename, validation: .allowing("hi"))
452 | var reqCompVal: String
453 |
454 | @Param(completion: .filename, validation: .allowing("yo"))
455 | var optCompVal: String?
456 |
457 | @Param
458 | var reqNone: String
459 |
460 | @Param
461 | var optNone: String?
462 |
463 | func execute() {}
464 | }
465 |
466 | // MARK: -
467 |
468 | func XCTAssertThrowsSpecificError(
469 | expression: @escaping @autoclosure () throws -> T,
470 | file: StaticString = #file,
471 | line: UInt = #line,
472 | error errorHandler: @escaping (E) -> Void) {
473 | XCTAssertThrowsError(try expression(), file: file, line: line) { (error) in
474 | guard let specificError = error as? E else {
475 | XCTFail("Error must be type \(String(describing: E.self)), is \(String(describing: type(of: error)))", file: file, line: line)
476 | return
477 | }
478 | errorHandler(specificError)
479 | }
480 | }
481 |
482 | func XCTAssertEqualLineByLine(_ s1: String, _ s2: String, file: StaticString = #file, line: UInt = #line) {
483 | let lines1 = s1.components(separatedBy: "\n")
484 | let lines2 = s2.components(separatedBy: "\n")
485 |
486 | XCTAssertEqual(lines1.count, lines2.count, "line count should be equal", file: file, line: line)
487 |
488 | for (l1, l2) in zip(lines1, lines2) {
489 | XCTAssertEqual(l1, l2, file: file, line: line)
490 | }
491 | }
492 |
493 | extension CLI {
494 |
495 | static func capture(_ block: () -> ()) -> (String, String) {
496 | let out = CaptureStream()
497 | let err = CaptureStream()
498 |
499 | Term.stdout = out
500 | Term.stderr = err
501 | block()
502 | Term.stdout = WriteStream.stdout
503 | Term.stderr = WriteStream.stderr
504 |
505 | out.closeWrite()
506 | err.closeWrite()
507 |
508 | return (out.readAll(), err.readAll())
509 | }
510 |
511 | }
512 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/InputTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputTests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 3/15/18.
6 | //
7 |
8 | import XCTest
9 | @testable import SwiftCLI
10 |
11 | class InputTests: XCTestCase {
12 |
13 | private var input: [String] = []
14 |
15 | override func setUp() {
16 | super.setUp()
17 |
18 | Term.read = {
19 | return self.input.removeFirst()
20 | }
21 | }
22 |
23 | func testInt() {
24 | input = ["", "asdf", "3.4", "5"]
25 | let (out, err) = CLI.capture {
26 | let int = Input.readInt()
27 | XCTAssertEqual(int, 5)
28 | }
29 | XCTAssertEqual(out, "")
30 | XCTAssertEqual(err, """
31 | Invalid value; expected Int
32 | Invalid value; expected Int
33 | Invalid value; expected Int
34 |
35 | """)
36 | }
37 |
38 | func testDouble() {
39 | input = ["", "asdf", "3.4", "5"]
40 | let (out, err) = CLI.capture {
41 | let double = Input.readDouble()
42 | XCTAssertEqual(double, 3.4)
43 | }
44 | XCTAssertEqual(out, "")
45 | XCTAssertEqual(err, """
46 | Invalid value; expected Double
47 | Invalid value; expected Double
48 |
49 | """)
50 | }
51 |
52 | func testBool() {
53 | input = ["", "asdf", "5", "false"]
54 | let (out, err) = CLI.capture {
55 | let bool = Input.readBool()
56 | XCTAssertEqual(bool, false)
57 | }
58 | XCTAssertEqual(out, "")
59 | XCTAssertEqual(err, """
60 | Invalid value; expected Bool
61 | Invalid value; expected Bool
62 | Invalid value; expected Bool
63 |
64 | """)
65 |
66 | input = ["asdf", "5", "T"]
67 | let (out2, err2) = CLI.capture {
68 | let bool = Input.readBool()
69 | XCTAssertEqual(bool, true)
70 | }
71 | XCTAssertEqual(out2, "")
72 | XCTAssertEqual(err2, """
73 | Invalid value; expected Bool
74 | Invalid value; expected Bool
75 |
76 | """)
77 |
78 | input = ["asdf", "yeppp", "YES"]
79 | let (out3, err3) = CLI.capture {
80 | let bool = Input.readBool()
81 | XCTAssertEqual(bool, true)
82 | }
83 | XCTAssertEqual(out3, "")
84 | XCTAssertEqual(err3, """
85 | Invalid value; expected Bool
86 | Invalid value; expected Bool
87 |
88 | """)
89 | }
90 |
91 | func testDefaultValue() {
92 | runTestCases(valid: [("5", 5), ("\n", 5)], invalid: ["invalid", "3.4"], expectedErrorOutput: "Invalid value; expected Int") {
93 | Input.readInt(defaultValue: 5)
94 | }
95 | runTestCases(valid: [("3.4", 3.4), ("5", 5), ("", 3.4)], invalid: ["invalid"], expectedErrorOutput: "Invalid value; expected Double") {
96 | Input.readDouble(defaultValue: 3.4)
97 | }
98 | runTestCases(valid: [("false", false), ("true", true), ("", false)], invalid: ["invalid", "3.4", "5"], expectedErrorOutput: "Invalid value; expected Bool") {
99 | Input.readBool(defaultValue: false)
100 | }
101 | runTestCases(valid: [("false", false), ("true", true), ("", true)], invalid: ["invalid", "3.4", "5"], expectedErrorOutput: "Invalid value; expected Bool") {
102 | Input.readBool(defaultValue: true)
103 | }
104 | }
105 |
106 | func testValidation() {
107 | input = ["", "asdf", "3.4", "5", "9", "11"]
108 | let (out, err) = CLI.capture {
109 | let int = Input.readInt(validation: [.greaterThan(10)])
110 | XCTAssertEqual(int, 11)
111 | }
112 | XCTAssertEqual(out, "")
113 | XCTAssertEqual(err, """
114 | Invalid value; expected Int
115 | Invalid value; expected Int
116 | Invalid value; expected Int
117 | Invalid value; must be greater than 10
118 | Invalid value; must be greater than 10
119 |
120 | """)
121 |
122 | input = ["", "asdf", "5", "false", "SwiftCLI"]
123 | let (out2, err2) = CLI.capture {
124 | let str = Input.readLine(validation: [.contains("ift")])
125 | XCTAssertEqual(str, "SwiftCLI")
126 | }
127 | XCTAssertEqual(out2, "")
128 | XCTAssertEqual(err2, """
129 | Invalid value; must contain 'ift'
130 | Invalid value; must contain 'ift'
131 | Invalid value; must contain 'ift'
132 | Invalid value; must contain 'ift'
133 |
134 | """)
135 | }
136 |
137 | private func runTestCases(valid validCases: [(String, T)], invalid invalidCases: [String], expectedErrorOutput: String, block: () -> T) {
138 | precondition(!validCases.isEmpty)
139 |
140 | for (validCase, expectedValue) in validCases {
141 | input = [validCase]
142 | let (out, err) = CLI.capture {
143 | XCTAssertEqual(block(), expectedValue)
144 | }
145 | XCTAssertEqual(out, "")
146 | XCTAssertEqual(err, "")
147 | }
148 |
149 | // NOTE: Input needs to end in a valid entry so it completes.
150 | input = invalidCases + [validCases[0].0]
151 |
152 | let (out, err) = CLI.capture {
153 | _ = block()
154 | }
155 | XCTAssertEqual(out, "")
156 | XCTAssertEqual(err, String(repeating: expectedErrorOutput + "\n", count: invalidCases.count))
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/OptionRegistryTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OptionRegistryTests.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 3/29/17.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SwiftCLI
11 |
12 | class OptionRegistryTests: XCTestCase {
13 |
14 | func testFlagDetection() {
15 | let cmd = FlagCmd()
16 | let options = OptionRegistry(routable: cmd)
17 | XCTAssert(options.flag(for: "-a") != nil, "Options should expect flags after a call to onFlags")
18 | XCTAssert(options.flag(for: "--alpha") != nil, "Options should expect flags after a call to onFlags")
19 | XCTAssert(options.key(for: "-a") == nil, "Options should parse no keys from only flags")
20 | }
21 |
22 | func testKeyDetection() {
23 | let cmd = KeyCmd()
24 | let options = OptionRegistry(routable: cmd)
25 | XCTAssert(options.key(for: "-a") != nil, "Options should expect keys after a call to onKeys")
26 | XCTAssert(options.key(for: "--alpha") != nil, "Options should expect keys after a call to onKeys")
27 | XCTAssert(options.flag(for: "-a") == nil, "Options should parse no flags from only keys")
28 | }
29 |
30 | func testVariadicDetection() {
31 | let cmd = VariadicKeyCmd()
32 | let options = OptionRegistry(routable: cmd)
33 | XCTAssertNotNil(options.key(for: "-f"))
34 | XCTAssertNotNil(options.key(for: "--file"))
35 | }
36 |
37 | func testMultipleRestrictions() {
38 | let cmd = MultipleRestrictionsCmd()
39 | let registry = OptionRegistry(routable: cmd)
40 | _ = registry.flag(for: "-a")
41 | _ = registry.flag(for: "-b")
42 |
43 | XCTAssertFalse(cmd.atMostOne.check())
44 | XCTAssertFalse(cmd.atMostOneAgain.check())
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/ParameterFillerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParameterFillerTests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 4/18/18.
6 | //
7 |
8 | import XCTest
9 | import SwiftCLI
10 |
11 | class ParameterFillerTests: XCTestCase {
12 |
13 | func testEmptySignature() throws {
14 | try parse(command: EmptyCmd(), args: [])
15 |
16 | assertParseNumberError(command: EmptyCmd(), args: ["arg"], min: 0, max: 0)
17 | }
18 |
19 | func testRequiredParameters() throws {
20 | assertParseNumberError(command: Req2Cmd(), args: ["arg"], min: 2, max: 2)
21 |
22 | let req2 = try parse(command: Req2Cmd(), args: ["arg1", "arg2"])
23 | XCTAssertEqual(req2.req1, "arg1")
24 | XCTAssertEqual(req2.req2, "arg2")
25 |
26 | assertParseNumberError(command: Req2Cmd(), args: ["arg1", "arg2", "arg3"], min: 2, max: 2)
27 | }
28 |
29 | func testOptionalParameters() throws {
30 | let cmd1 = try parse(command: Opt2Cmd(), args: [])
31 | XCTAssertEqual(cmd1.opt1, nil)
32 | XCTAssertEqual(cmd1.opt2, nil)
33 |
34 | let cmd2 = try parse(command: Opt2Cmd(), args: ["arg1"])
35 | XCTAssertEqual(cmd2.opt1, "arg1")
36 | XCTAssertEqual(cmd2.opt2, nil)
37 |
38 | let cmd3 = try parse(command: Opt2Cmd(), args: ["arg1", "arg2"])
39 | XCTAssertEqual(cmd3.opt1, "arg1")
40 | XCTAssertEqual(cmd3.opt2, "arg2")
41 |
42 | assertParseNumberError(command: Opt2Cmd(), args: ["arg1", "arg2", "arg3"], min: 0, max: 2)
43 | }
44 |
45 | func testOptionalParametersWithInheritance() throws {
46 | let cmd1 = try parse(command: Opt2InhCmd(), args: [])
47 | XCTAssertEqual(cmd1.opt1, nil)
48 | XCTAssertEqual(cmd1.opt2, nil)
49 | XCTAssertEqual(cmd1.opt3, nil)
50 |
51 | let cmd2 = try parse(command: Opt2InhCmd(), args: ["arg1"])
52 | XCTAssertEqual(cmd2.opt1, "arg1")
53 | XCTAssertEqual(cmd2.opt2, nil)
54 | XCTAssertEqual(cmd2.opt3, nil)
55 |
56 | let cmd3 = try parse(command: Opt2InhCmd(), args: ["arg1", "arg2"])
57 | XCTAssertEqual(cmd3.opt1, "arg1")
58 | XCTAssertEqual(cmd3.opt2, "arg2")
59 | XCTAssertEqual(cmd3.opt3, nil)
60 |
61 | let cmd4 = try parse(command: Opt2InhCmd(), args: ["arg1", "arg2", "arg3"])
62 | XCTAssertEqual(cmd4.opt1, "arg1")
63 | XCTAssertEqual(cmd4.opt2, "arg2")
64 | XCTAssertEqual(cmd4.opt3, "arg3")
65 |
66 | assertParseNumberError(command: Opt2InhCmd(), args: ["arg1", "arg2", "arg3", "arg4"], min: 0, max: 3)
67 | }
68 |
69 | func testCollectedRequiredParameters() throws {
70 | assertParseNumberError(command: ReqCollectedCmd(), args: [], min: 1, max: nil)
71 |
72 | assertParseNumberError(command: Req2CollectedCmd(), args: ["arg1"], min: 2, max: nil)
73 |
74 | let cmd1 = try parse(command: Req2CollectedCmd(), args: ["arg1", "arg2"])
75 | XCTAssertEqual(cmd1.req1, "arg1")
76 | XCTAssertEqual(cmd1.req2, ["arg2"])
77 |
78 | let cmd2 = try parse(command: Req2CollectedCmd(), args: ["arg1", "arg2", "arg3"])
79 | XCTAssertEqual(cmd2.req1, "arg1")
80 | XCTAssertEqual(cmd2.req2, ["arg2", "arg3"])
81 |
82 | assertParseNumberError(command: TriReqCollectedCmd(), args: ["arg1"], min: 3, max: nil)
83 | assertParseNumberError(command: TriReqCollectedCmd(), args: ["arg1", "arg2"], min: 3, max: nil)
84 |
85 | let cmd3 = try parse(command: TriReqCollectedCmd(), args: ["arg1", "arg2", "arg3"])
86 | XCTAssertEqual(cmd3.triReq, ["arg1", "arg2", "arg3"])
87 | }
88 |
89 | func testCollectedOptionalParameters() throws {
90 | let cmd1 = try parse(command: Opt2CollectedCmd(), args: [])
91 | XCTAssertEqual(cmd1.opt1, nil)
92 | XCTAssertEqual(cmd1.opt2, [])
93 |
94 | let cmd2 = try parse(command: Opt2CollectedCmd(), args: ["arg1"])
95 | XCTAssertEqual(cmd2.opt1, "arg1")
96 | XCTAssertEqual(cmd2.opt2, [])
97 |
98 | let cmd3 = try parse(command: Opt2CollectedCmd(), args: ["arg1", "arg2"])
99 | XCTAssertEqual(cmd3.opt1, "arg1")
100 | XCTAssertEqual(cmd3.opt2, ["arg2"])
101 |
102 | let cmd4 = try parse(command: Opt2CollectedCmd(), args: ["arg1", "arg2", "arg3"])
103 | XCTAssertEqual(cmd4.opt1, "arg1")
104 | XCTAssertEqual(cmd4.opt2, ["arg2", "arg3"])
105 | }
106 |
107 | func testCombinedRequiredAndOptionalParameters() throws {
108 | assertParseNumberError(command: Req2Opt2Cmd(), args: ["arg1"], min: 2, max: 4)
109 |
110 | let cmd1 = try parse(command: Req2Opt2Cmd(), args: ["arg1", "arg2"])
111 | XCTAssertEqual(cmd1.req1, "arg1")
112 | XCTAssertEqual(cmd1.req2, "arg2")
113 | XCTAssertNil(cmd1.opt1)
114 | XCTAssertNil(cmd1.opt2)
115 |
116 | let cmd2 = try parse(command: Req2Opt2Cmd(), args: ["arg1", "arg2", "arg3"])
117 | XCTAssertEqual(cmd2.req1, "arg1")
118 | XCTAssertEqual(cmd2.req2, "arg2")
119 | XCTAssertEqual(cmd2.opt1, "arg3")
120 | XCTAssertNil(cmd2.opt2)
121 |
122 | let cmd3 = try parse(command: Req2Opt2Cmd(), args: ["arg1", "arg2", "arg3", "arg4"])
123 | XCTAssertEqual(cmd3.req1, "arg1")
124 | XCTAssertEqual(cmd3.req2, "arg2")
125 | XCTAssertEqual(cmd3.opt1, "arg3")
126 | XCTAssertEqual(cmd3.opt2, "arg4")
127 |
128 | assertParseNumberError(command: Req2Opt2Cmd(), args: ["arg1", "arg2", "arg3", "arg4", "arg5"], min: 2, max: 4)
129 | }
130 |
131 | func testEmptyOptionalCollectedParameter() throws { // Tests regression
132 | let cmd = try parse(command: OptCollectedCmd(), args: [])
133 | XCTAssertEqual(cmd.opt1, [])
134 | }
135 |
136 | func testCustomParameter() throws {
137 | assertParseNumberError(command: EnumCmd(), args: [], min: 1, max: 3)
138 |
139 | let cmd = EnumCmd()
140 | XCTAssertThrowsSpecificError(
141 | expression: try self.parse(command: cmd, args: ["value"]),
142 | error: { (error: ParameterError) in
143 | guard case .invalidValue(let namedParam, .conversionError) = error.kind else {
144 | XCTFail()
145 | return
146 | }
147 |
148 | XCTAssertEqual(namedParam.name, "speed")
149 | XCTAssert(namedParam.param === cmd.$speed)
150 | })
151 |
152 | let fast = try parse(command: EnumCmd(), args: ["fast"])
153 | XCTAssertEqual(fast.speed.rawValue, "fast")
154 |
155 | let slow = try parse(command: EnumCmd(), args: ["slow"])
156 | XCTAssertEqual(slow.speed.rawValue, "slow")
157 |
158 | assertParseNumberError(command: EnumCmd(), args: ["slow", "value", "3", "fourth"], min: 1, max: 3)
159 |
160 | let cmd2 = EnumCmd()
161 | XCTAssertThrowsSpecificError(
162 | expression: try self.parse(command: cmd2, args: ["slow", "other"]),
163 | error: { (error: ParameterError) in
164 | guard case .invalidValue(let namedParam, .conversionError) = error.kind else {
165 | XCTFail()
166 | return
167 | }
168 |
169 | XCTAssertEqual(namedParam.name, "single")
170 | XCTAssert(ObjectIdentifier(namedParam.param) == ObjectIdentifier(cmd2.$single))
171 | })
172 | }
173 |
174 | func testValidatedParameter() throws {
175 | let cmd1 = try parse(command: ValidatedParamCmd(), args: [])
176 | XCTAssertNil(cmd1.age)
177 |
178 | let cmd2 = ValidatedParamCmd()
179 | XCTAssertThrowsSpecificError(
180 | expression: try self.parse(command: cmd2, args: ["16"]),
181 | error: { (error: ParameterError) in
182 | guard case .invalidValue(let namedParam, .validationError(let validation)) = error.kind else {
183 | XCTFail()
184 | return
185 | }
186 |
187 | XCTAssertEqual(namedParam.name, "age")
188 | XCTAssertEqual(ObjectIdentifier(namedParam.param), ObjectIdentifier(cmd2.$age))
189 | XCTAssertEqual(validation.message, "must be greater than 18")
190 | })
191 |
192 | let cmd3 = try parse(command: ValidatedParamCmd(), args: ["20"])
193 | XCTAssertEqual(cmd3.age, 20)
194 | }
195 |
196 | func testParameterInit() {
197 | let cmd = ParamInitCmd()
198 |
199 | XCTAssertTrue(cmd.$reqComp.required)
200 | XCTAssertTrue(cmd.$reqVal.required)
201 | XCTAssertTrue(cmd.$reqCompVal.required)
202 | XCTAssertTrue(cmd.$reqNone.required)
203 |
204 | XCTAssertFalse(cmd.$optComp.required)
205 | XCTAssertFalse(cmd.$optVal.required)
206 | XCTAssertFalse(cmd.$optCompVal.required)
207 | XCTAssertFalse(cmd.$optNone.required)
208 |
209 | XCTAssertNil(cmd.optComp)
210 | XCTAssertNil(cmd.optVal)
211 | XCTAssertNil(cmd.optCompVal)
212 | XCTAssertNil(cmd.optNone)
213 | }
214 |
215 | // MARK: -
216 |
217 | @discardableResult
218 | private func parse(command: T, args: [String]) throws -> T {
219 | let cli = CLI(name: "tester", commands: [command])
220 | let arguments = ArgumentList(arguments: [command.name] + args)
221 | let routed = try Parser().parse(cli: cli, arguments: arguments)
222 | XCTAssert(routed.command === command)
223 |
224 | return command
225 | }
226 |
227 | private func assertParseNumberError(command: T, args: [String], min: Int, max: Int?, file: StaticString = #file, line: UInt = #line) {
228 | XCTAssertThrowsSpecificError(
229 | expression: try self.parse(command: command, args: args), file: file, line: line,
230 | error: { (error: ParameterError) in
231 | guard case let .wrongNumber(aMin, aMax) = error.kind else {
232 | XCTFail("Expected error to be .wrongNumber(\(min), \(max as Any)); got .\(error.kind)", file: file, line: line)
233 | return
234 | }
235 | XCTAssertEqual(aMin, min, file: file, line: line)
236 | XCTAssertEqual(aMax, max, file: file, line: line)
237 | })
238 | }
239 |
240 | }
241 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/ParserTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ParserTests.swift
3 | // SwiftCLI
4 | //
5 | // Created by Jake Heiser on 1/7/15.
6 | // Copyright (c) 2015 jakeheis. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import SwiftCLI
11 |
12 | class ParserTests: XCTestCase {
13 |
14 | // MARK: - Option parsing tests
15 |
16 | func testSimpleFlagParsing() throws {
17 | let cmd = DoubleFlagCmd()
18 | let arguments = ArgumentList(arguments: ["cmd", "-a", "-b"])
19 | let cli = CLI.createTester(commands: [cmd])
20 |
21 | _ = try Parser().parse(cli: cli, arguments: arguments)
22 | XCTAssertTrue(cmd.alpha)
23 | XCTAssertTrue(cmd.beta)
24 | }
25 |
26 | func testSimpleKeyParsing() throws {
27 | let cmd = DoubleKeyCmd()
28 | let arguments = ArgumentList(arguments: ["cmd", "-a", "apple", "-b", "banana"])
29 | let cli = CLI.createTester(commands: [cmd])
30 |
31 | _ = try Parser().parse(cli: cli, arguments: arguments)
32 |
33 | XCTAssertEqual(cmd.alpha, "apple", "Options should update the values of passed keys")
34 | XCTAssertEqual(cmd.beta, "banana", "Options should update the values of passed keys")
35 | }
36 |
37 | func testKeyValueParsing() throws {
38 | let cmd = IntKeyCmd()
39 | let arguments = ArgumentList(arguments: ["cmd", "-a", "7"])
40 | let cli = CLI.createTester(commands: [cmd])
41 |
42 | _ = try Parser().parse(cli: cli, arguments: arguments)
43 |
44 | XCTAssertEqual(cmd.alpha, 7, "Options should parse int")
45 | }
46 |
47 | func testCombinedFlagsAndKeysParsing() throws {
48 | let cmd = FlagKeyCmd()
49 | let arguments = ArgumentList(arguments: ["cmd", "-a", "-b", "banana"])
50 | let cli = CLI.createTester(commands: [cmd])
51 |
52 | _ = try Parser().parse(cli: cli, arguments: arguments)
53 |
54 | XCTAssertTrue(cmd.alpha, "Options should execute the closures of passed flags")
55 | XCTAssertEqual(cmd.beta, "banana", "Options should execute the closures of passed keys")
56 |
57 | let cmd2 = FlagKeyCmd()
58 | let arguments2 = ArgumentList(arguments: ["cmd", "-ab", "banana"])
59 | let cli2 = CLI.createTester(commands: [cmd2])
60 |
61 | _ = try Parser().parse(cli: cli2, arguments: arguments2)
62 |
63 | XCTAssertTrue(cmd2.alpha)
64 | XCTAssertEqual(cmd2.beta, "banana")
65 | }
66 |
67 | func testCombinedFlagsAndKeysAndArgumentsParsing() throws {
68 | let cmd = FlagKeyParamCmd()
69 | let arguments = ArgumentList(arguments: ["cmd", "-a", "argument", "-b", "banana"])
70 | let cli = CLI.createTester(commands: [cmd])
71 |
72 | _ = try Parser().parse(cli: cli, arguments: arguments)
73 |
74 | XCTAssert(cmd.alpha, "Options should execute the closures of passed flags")
75 | XCTAssertEqual(cmd.beta, "banana", "Options should execute the closures of passed keys")
76 | XCTAssertEqual(cmd.param, "argument")
77 | }
78 |
79 | func testUnrecognizedOptions() throws {
80 | let cmd = FlagCmd()
81 | let arguments = ArgumentList(arguments: ["cmd", "-a", "-b"])
82 | let cli = CLI.createTester(commands: [cmd])
83 |
84 | do {
85 | _ = try Parser().parse(cli: cli, arguments: arguments)
86 | XCTFail()
87 | } catch let error as OptionError {
88 | guard case let .unrecognizedOption(key) = error.kind, key == "-b" else {
89 | XCTFail()
90 | return
91 | }
92 | }
93 | }
94 |
95 | func testKeysNotGivenValues() throws {
96 | let cmd = FlagKeyCmd()
97 | let arguments = ArgumentList(arguments: ["cmd", "-b", "-a"])
98 | let cli = CLI.createTester(commands: [cmd])
99 |
100 | do {
101 | _ = try Parser().parse(cli: cli, arguments: arguments)
102 | XCTFail()
103 | } catch let error as OptionError {
104 | guard case let .expectedValueAfterKey(key) = error.kind else {
105 | XCTFail()
106 | return
107 | }
108 | XCTAssertEqual(key, "-b")
109 | }
110 |
111 | let cmd2 = FlagKeyCmd()
112 | let arguments2 = ArgumentList(arguments: ["cmd", "-ba"])
113 | let cli2 = CLI.createTester(commands: [cmd2])
114 |
115 | do {
116 | _ = try Parser().parse(cli: cli2, arguments: arguments2)
117 | XCTFail()
118 | } catch let error as OptionError {
119 | guard case let .expectedValueAfterKey(key) = error.kind else {
120 | XCTFail()
121 | return
122 | }
123 | XCTAssertEqual(key, "-b")
124 | }
125 | }
126 |
127 | func testFlagGivenValue() throws {
128 | let cmd = FlagKeyCmd()
129 | let arguments = ArgumentList(arguments: ["cmd", "--alpha=value"])
130 | let cli = CLI.createTester(commands: [cmd])
131 |
132 | do {
133 | _ = try Parser().parse(cli: cli, arguments: arguments)
134 | XCTFail()
135 | } catch let error as OptionError {
136 | guard case let .unexpectedValueAfterFlag(flag) = error.kind else {
137 | XCTFail()
138 | return
139 | }
140 | XCTAssertEqual(flag, "--alpha")
141 | }
142 | }
143 |
144 | func testIllegalOptionFormat() throws {
145 | let cmd = IntKeyCmd()
146 | let arguments = ArgumentList(arguments: ["cmd", "-a", "val"])
147 | let cli = CLI.createTester(commands: [cmd])
148 |
149 | do {
150 | _ = try Parser().parse(cli: cli, arguments: arguments)
151 | XCTFail()
152 | } catch let error as OptionError {
153 | guard case .invalidKeyValue(let key, "-a", .conversionError) = error.kind, ObjectIdentifier(key) == ObjectIdentifier(cmd.$alpha) else {
154 | XCTFail()
155 | return
156 | }
157 | }
158 | }
159 |
160 | func testFlagSplitting() throws {
161 | let cmd = DoubleFlagCmd()
162 | let arguments = ArgumentList(arguments: ["cmd", "-ab"])
163 | let cli = CLI.createTester(commands: [cmd])
164 |
165 | _ = try Parser().parse(cli: cli, arguments: arguments)
166 |
167 | XCTAssertTrue(cmd.alpha)
168 | XCTAssertTrue(cmd.beta)
169 | }
170 |
171 | func testGroupRestriction() throws {
172 | let cmd1 = ExactlyOneCmd()
173 | let arguments1 = ArgumentList(arguments: ["cmd", "-a", "-b"])
174 |
175 | do {
176 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd1]), arguments: arguments1)
177 | XCTFail()
178 | return
179 | } catch let error as OptionError {
180 | guard case let .optionGroupMisuse(group) = error.kind else {
181 | XCTFail()
182 | return
183 | }
184 | XCTAssert(group === cmd1.optionGroups[0])
185 | }
186 |
187 | let cmd2 = ExactlyOneCmd()
188 | let arguments2 = ArgumentList(arguments: ["cmd", "-a"])
189 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd2]), arguments: arguments2)
190 | XCTAssertTrue(cmd2.alpha)
191 | XCTAssertFalse(cmd2.beta)
192 |
193 | let cmd3 = ExactlyOneCmd()
194 | let arguments3 = ArgumentList(arguments: ["cmd", "-b"])
195 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd3]), arguments: arguments3)
196 | XCTAssertTrue(cmd3.beta)
197 | XCTAssertFalse(cmd3.alpha)
198 |
199 | let cmd4 = ExactlyOneCmd()
200 | let arguments4 = ArgumentList(arguments: ["cmd"])
201 | do {
202 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd4]), arguments: arguments4)
203 | XCTFail()
204 | } catch let error as OptionError {
205 | guard case let .optionGroupMisuse(group) = error.kind else {
206 | XCTFail()
207 | return
208 | }
209 | XCTAssert(group === cmd4.optionGroups[0])
210 | }
211 | }
212 |
213 | func testVaridadicParse() throws {
214 | let cmd = VariadicKeyCmd()
215 | let cli = CLI.createTester(commands: [cmd])
216 | let arguments = ArgumentList(arguments: ["cmd", "-f", "firstFile", "--file", "secondFile"])
217 |
218 | _ = try Parser().parse(cli: cli, arguments: arguments)
219 | XCTAssertEqual(cmd.files, ["firstFile", "secondFile"])
220 | }
221 |
222 | func testCounterParse() throws {
223 | let counterCmd = CounterFlagCmd()
224 | let counterCli = CLI.createTester(commands: [counterCmd])
225 | _ = try Parser().parse(cli: counterCli, arguments: ArgumentList(arguments: ["cmd", "-v", "-v"]))
226 | XCTAssertEqual(counterCmd.verbosity, 2)
227 |
228 | let flagCmd = FlagCmd()
229 | let flagCli = CLI.createTester(commands: [flagCmd])
230 | _ = try Parser().parse(cli: flagCli, arguments: ArgumentList(arguments: ["cmd", "-a", "-a"]))
231 | XCTAssertTrue(flagCmd.flag)
232 | }
233 |
234 | func testBeforeCommand() throws {
235 | let cmd = EmptyCmd()
236 | let yes = Flag("-y")
237 |
238 | let cli = CLI.createTester(commands: [cmd])
239 | cli.globalOptions = [yes]
240 | let arguments = ArgumentList(arguments: ["-y", "cmd"])
241 |
242 | _ = try Parser().parse(cli: cli, arguments: arguments)
243 | XCTAssertTrue(yes.wrappedValue)
244 | }
245 |
246 | func testValidation() throws {
247 | let cmd1 = ValidatedKeyCmd()
248 | let arguments1 = ArgumentList(arguments: ["cmd", "-n", "jake"])
249 |
250 | do {
251 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd1]), arguments: arguments1)
252 | XCTFail()
253 | } catch let error as OptionError {
254 | guard case .invalidKeyValue(let key, "-n", .validationError(let validator)) = error.kind else {
255 | XCTFail()
256 | return
257 | }
258 | XCTAssert(ObjectIdentifier(key) == ObjectIdentifier(cmd1.$firstName))
259 | XCTAssertEqual(validator.message, "Must be a capitalized first name")
260 | }
261 |
262 | let cmd2 = ValidatedKeyCmd()
263 | let arguments2 = ArgumentList(arguments: ["cmd", "-n", "Jake"])
264 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd2]), arguments: arguments2)
265 | XCTAssertEqual(cmd2.firstName, "Jake")
266 |
267 | let cmd3 = ValidatedKeyCmd()
268 | let arguments3 = ArgumentList(arguments: ["cmd", "-a", "15"])
269 |
270 | do {
271 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd3]), arguments: arguments3)
272 | XCTFail()
273 | } catch let error as OptionError {
274 | guard case .invalidKeyValue(let key, "-a", .validationError(let validator)) = error.kind else {
275 | XCTFail(String(describing: error))
276 | return
277 | }
278 | XCTAssert(ObjectIdentifier(key) == ObjectIdentifier(cmd3.$age))
279 | XCTAssertEqual(validator.message, "must be greater than 18")
280 | }
281 |
282 | let cmd4 = ValidatedKeyCmd()
283 | let arguments4 = ArgumentList(arguments: ["cmd", "-a", "19"])
284 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd4]), arguments: arguments4)
285 | XCTAssertEqual(cmd4.age, 19)
286 |
287 | let cmd5 = ValidatedKeyCmd()
288 | let arguments5 = ArgumentList(arguments: ["cmd", "-l", "Chicago"])
289 |
290 | do {
291 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd5]), arguments: arguments5)
292 | XCTFail()
293 | } catch let error as OptionError {
294 | guard case .invalidKeyValue(let key, "-l", .validationError(let validator)) = error.kind else {
295 | XCTFail()
296 | return
297 | }
298 |
299 | XCTAssert(ObjectIdentifier(key) == ObjectIdentifier(cmd5.$location))
300 | XCTAssertEqual(validator.message, "must not be: Chicago, Boston")
301 | }
302 |
303 | let cmd6 = ValidatedKeyCmd()
304 | let arguments6 = ArgumentList(arguments: ["cmd", "-l", "Denver"])
305 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd6]), arguments: arguments6)
306 | XCTAssertEqual(cmd6.location, "Denver")
307 |
308 | let cmd7 = ValidatedKeyCmd()
309 | let arguments7 = ArgumentList(arguments: ["cmd", "--holiday", "4th"])
310 |
311 | do {
312 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd7]), arguments: arguments7)
313 | XCTFail()
314 | } catch let error as OptionError {
315 | guard case .invalidKeyValue(let key, "--holiday", .validationError(let validator)) = error.kind else {
316 | XCTFail(String(describing: error))
317 | return
318 | }
319 | XCTAssert(ObjectIdentifier(key) == ObjectIdentifier(cmd7.$holiday))
320 | XCTAssertEqual(validator.message, "must be one of: Thanksgiving, Halloween")
321 | }
322 |
323 | let cmd8 = ValidatedKeyCmd()
324 | let arguments8 = ArgumentList(arguments: ["cmd", "--holiday", "Thanksgiving"])
325 | _ = try Parser().parse(cli: CLI.createTester(commands: [cmd8]), arguments: arguments8)
326 | XCTAssertEqual(cmd8.holiday, "Thanksgiving")
327 | }
328 |
329 | // MARK: - Combined test
330 |
331 | func testFullParse() throws {
332 | let cmd = TestCommand()
333 | let cli = CLI.createTester(commands: [cmd])
334 |
335 | let args = ArgumentList(arguments: ["test", "-s", "favTest", "-t", "3", "SwiftCLI"])
336 | let result = try Parser().parse(cli: cli, arguments: args)
337 |
338 | XCTAssertTrue(result.command === cmd)
339 |
340 | XCTAssertEqual(cmd.testName, "favTest")
341 | XCTAssertEqual(cmd.testerName, "SwiftCLI")
342 | XCTAssertTrue(cmd.silent)
343 | XCTAssertEqual(cmd.times, 3)
344 | }
345 |
346 | func testCollectedOptions() throws {
347 | class RunCmd: Command {
348 | let name = "run"
349 | @Param var executable: String
350 | @CollectedParam var args: [String]
351 | @Flag("-v") var verbose: Bool
352 | func execute() throws {}
353 | }
354 |
355 | let cmd = RunCmd()
356 | let cli = CLI.createTester(commands: [cmd])
357 | let args = ArgumentList(arguments: ["run", "cli", "-v", "arg"])
358 |
359 | let result = try Parser().parse(cli: cli, arguments: args)
360 | XCTAssertTrue(result.command === cmd)
361 |
362 | XCTAssertEqual(cmd.executable, "cli")
363 | XCTAssertEqual(cmd.args, ["-v", "arg"])
364 | XCTAssertFalse(cmd.verbose)
365 |
366 | let cmd2 = RunCmd()
367 | let cli2 = CLI.createTester(commands: [cmd2])
368 | let args2 = ArgumentList(arguments: ["run", "-v", "cli", "arg"])
369 |
370 | let result2 = try Parser().parse(cli: cli2, arguments: args2)
371 | XCTAssertTrue(result2.command === cmd2)
372 |
373 | XCTAssertEqual(cmd2.executable, "cli")
374 | XCTAssertEqual(cmd2.args, ["arg"])
375 | XCTAssertTrue(cmd2.verbose)
376 |
377 | let cmd3 = RunCmd()
378 | let cli3 = CLI.createTester(commands: [cmd3])
379 | let args3 = ArgumentList(arguments: ["run", "cli", "-v", "arg"])
380 |
381 | var parser = Parser()
382 | parser.parseOptionsAfterCollectedParameter = true
383 | let result3 = try parser.parse(cli: cli3, arguments: args3)
384 | XCTAssertTrue(result3.command === cmd3)
385 |
386 | XCTAssertEqual(cmd3.executable, "cli")
387 | XCTAssertEqual(cmd3.args, ["arg"])
388 | XCTAssertTrue(cmd3.verbose)
389 | }
390 |
391 | }
392 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/RouterTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RouterTests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 4/18/18.
6 | //
7 |
8 | import XCTest
9 | import SwiftCLI
10 |
11 | class RouterTests: XCTestCase {
12 |
13 | func testNameRoute() throws {
14 | let args = ArgumentList(arguments: ["alpha"])
15 | let cli = CLI.createTester(commands: [alphaCmd, betaCmd])
16 |
17 | let command = try Parser().parse(cli: cli, arguments: args)
18 | XCTAssert(command.groupPath.bottom === cli, "Router should generate correct group path")
19 | XCTAssertEqual(command.groupPath.groups.count, 1, "Router should generate correct group path")
20 | XCTAssert(command.command === alphaCmd, "Router should route to the command with the given name")
21 | XCTAssertFalse(args.hasNext())
22 | }
23 |
24 | func testAliasRoute() throws {
25 | let args = ArgumentList(arguments: ["-b"])
26 |
27 | let cli = CLI.createTester(commands: [alphaCmd, betaCmd])
28 | cli.aliases["-b"] = betaCmd.name
29 |
30 | let command = try Parser().parse(cli: cli, arguments: args)
31 | XCTAssertEqual(command.groupPath.bottom.name, cli.name, "Router should generate correct group path")
32 | XCTAssertEqual(command.groupPath.groups.count, 1, "Router should generate correct group path")
33 | XCTAssertEqual(command.command.name, betaCmd.name, "Router with enabled shortcut routing should route to the command with the given shortcut")
34 | XCTAssertFalse(args.hasNext())
35 | }
36 |
37 | func testSingleRouter() throws {
38 | var parser = Parser()
39 |
40 | let cmd = FlagCmd()
41 | let cli = CLI.createTester(commands: [cmd])
42 | parser.routeBehavior = .automatically(cmd)
43 | let path = try parser.parse(cli: cli, arguments: ArgumentList(arguments: ["-a"]))
44 |
45 | XCTAssert(path.groupPath.bottom === cli, "Router should generate correct group path")
46 | XCTAssertTrue(path.ignoreName)
47 | XCTAssert(path.command === cmd, "Router should route to the single command")
48 | XCTAssertTrue(cmd.flag)
49 |
50 | let cmd2 = FlagCmd()
51 | let cli2 = CLI.createTester(commands: [cmd2])
52 | parser.routeBehavior = .automatically(cmd2)
53 | let path2 = try parser.parse(cli: cli2, arguments: ArgumentList(arguments: []))
54 |
55 | XCTAssert(path2.groupPath.bottom === cli2, "Router should generate correct group path")
56 | XCTAssertTrue(path2.ignoreName)
57 | XCTAssert(path2.command === cmd2, "Router should route to the single command")
58 | XCTAssertFalse(cmd2.flag)
59 | }
60 |
61 | func testFallbackOption() throws {
62 | var parser = Parser()
63 |
64 | let cmd = FlagCmd()
65 | let cli = CLI.createTester(commands: [cmd])
66 | parser.routeBehavior = .searchWithFallback(cmd)
67 | let path = try parser.parse(cli: cli, arguments: ArgumentList(arguments: ["-a"]))
68 |
69 | XCTAssert(path.groupPath.bottom === cli, "Router should generate correct group path")
70 | XCTAssertTrue(path.ignoreName)
71 | XCTAssert(path.command === cmd, "Router should route to the single command")
72 | XCTAssertTrue(cmd.flag)
73 |
74 | let cmd2 = FlagCmd()
75 | let cli2 = CLI.createTester(commands: [cmd2])
76 | parser.routeBehavior = .searchWithFallback(cmd2)
77 | let path2 = try parser.parse(cli: cli2, arguments: ArgumentList(arguments: []))
78 |
79 | XCTAssert(path2.groupPath.bottom === cli2, "Router should generate correct group path")
80 | XCTAssertTrue(path2.ignoreName)
81 | XCTAssert(path2.command === cmd2, "Router should route to the single command")
82 | XCTAssertFalse(cmd2.flag)
83 | }
84 |
85 | func testFailedRoute() throws {
86 | let args = ArgumentList(arguments: ["charlie"])
87 | let cli = CLI.createTester(commands: [alphaCmd, betaCmd])
88 |
89 | do {
90 | _ = try Parser().parse(cli: cli, arguments: args)
91 | XCTFail()
92 | } catch let error as RouteError {
93 | XCTAssert(error.partialPath.bottom === cli, "Router should generate correct group path")
94 | XCTAssertEqual(error.partialPath.groups.count, 1, "Router should generate correct group path")
95 | XCTAssertEqual(error.notFound, "charlie")
96 | }
97 | }
98 |
99 | func testGroupPartialRoute() throws {
100 | let arguments = ArgumentList(arguments: ["mid"])
101 | let cli = CLI.createTester(commands: [midGroup, intraGroup, Req2Cmd(), Opt2Cmd()])
102 |
103 | do {
104 | _ = try Parser().parse(cli: cli, arguments: arguments)
105 | XCTFail()
106 | } catch let error as RouteError {
107 | XCTAssert(error.partialPath.groups[0] === cli, "Router should generate correct group path")
108 | XCTAssert(error.partialPath.groups[1] === midGroup, "Router should generate correct group path")
109 | XCTAssertEqual(error.partialPath.groups.count, 2, "Router should generate correct group path")
110 | XCTAssertNil(error.notFound)
111 | }
112 | }
113 |
114 | func testGroupFailedRoute() throws {
115 | let arguments = ArgumentList(arguments: ["mid", "charlie"])
116 | let cli = CLI.createTester(commands: [midGroup, intraGroup, Req2Cmd(), Opt2Cmd()])
117 |
118 | do {
119 | _ = try Parser().parse(cli: cli, arguments: arguments)
120 | XCTFail()
121 | } catch let error as RouteError {
122 | XCTAssert(error.partialPath.groups[0] === cli, "Router should generate correct group path")
123 | XCTAssert(error.partialPath.groups[1] === midGroup, "Router should generate correct group path")
124 | XCTAssertEqual(error.partialPath.groups.count, 2, "Router should generate correct group path")
125 | XCTAssertEqual(error.notFound, "charlie")
126 | }
127 | }
128 |
129 | func testGroupSuccessRoute() throws {
130 | let arguments = ArgumentList(arguments: ["mid", "beta"])
131 | let cli = CLI.createTester(commands: [midGroup, intraGroup, Req2Cmd(), Opt2Cmd()])
132 |
133 | let cmd = try Parser().parse(cli: cli, arguments: arguments)
134 | XCTAssert(cmd.groupPath.groups[0] === cli, "Router should generate correct group path")
135 | XCTAssert(cmd.groupPath.bottom === midGroup, "Router should generate correct group path")
136 | XCTAssertEqual(cmd.groupPath.groups.count, 2, "Router should generate correct group path")
137 | XCTAssert(cmd.command === betaCmd)
138 | XCTAssertFalse(arguments.hasNext())
139 | }
140 |
141 | func testNestedGroup() throws {
142 | class Nested: CommandGroup {
143 | let name = "nested"
144 | let shortDescription = "Nested group"
145 | let children: [Routable] = [midGroup, intraGroup]
146 | }
147 |
148 | let nested = Nested()
149 |
150 | var arguments = ArgumentList(arguments: ["nested"])
151 | let cli = CLI.createTester(commands: [nested])
152 |
153 | do {
154 | _ = try Parser().parse(cli: cli, arguments: arguments)
155 | XCTFail()
156 | } catch let error as RouteError {
157 | XCTAssert(error.partialPath.groups[0] === cli, "Router should generate correct group path")
158 | XCTAssert(error.partialPath.bottom === nested, "Router should generate correct group path")
159 | XCTAssertEqual(error.partialPath.groups.count, 2, "Router should generate correct group path")
160 | XCTAssertNil(error.notFound)
161 | }
162 |
163 | arguments = ArgumentList(arguments: ["nested", "intra"])
164 | do {
165 | _ = try Parser().parse(cli: cli, arguments: arguments)
166 | XCTFail()
167 | } catch let error as RouteError {
168 | XCTAssert(error.partialPath.groups[0] === cli, "Router should generate correct group path")
169 | XCTAssert(error.partialPath.groups[1] === nested, "Router should generate correct group path")
170 | XCTAssert(error.partialPath.bottom === intraGroup, "Router should generate correct group path")
171 | XCTAssertEqual(error.partialPath.groups.count, 3, "Router should generate correct group path")
172 | XCTAssertNil(error.notFound)
173 | }
174 |
175 | arguments = ArgumentList(arguments: ["nested", "intra", "delta"])
176 | let cmd = try Parser().parse(cli: cli, arguments: arguments)
177 |
178 | XCTAssert(cmd.groupPath.groups[0] === cli, "Router should generate correct group path")
179 | XCTAssert(cmd.groupPath.groups[1] === nested, "Router should generate correct group path")
180 | XCTAssert(cmd.groupPath.bottom === intraGroup, "Router should generate correct group path")
181 | XCTAssertEqual(cmd.groupPath.groups.count, 3, "Router should generate correct group path")
182 | XCTAssert(cmd.command === deltaCmd)
183 | XCTAssertFalse(arguments.hasNext())
184 | }
185 |
186 | func testFallback() throws {
187 | func setup() -> (Opt1Cmd, CLI, Parser) {
188 | let opt1 = Opt1Cmd()
189 | let cli = CLI.createTester(commands: [opt1])
190 | var parser = Parser()
191 | parser.routeBehavior = .searchWithFallback(opt1)
192 | return (opt1, cli, parser)
193 | }
194 |
195 | let (opt1, cli1, parser1) = setup()
196 |
197 | let firstResult = try parser1.parse(cli: cli1, arguments: ArgumentList(arguments: ["cmd", "value"]))
198 | XCTAssert(opt1 === firstResult.command)
199 | XCTAssertFalse(firstResult.ignoreName)
200 | XCTAssertEqual(opt1.opt1, "value")
201 |
202 | let (opt2, cli2, parser2) = setup()
203 |
204 | let secondResult = try parser2.parse(cli: cli2, arguments: ArgumentList(arguments: ["value2"]))
205 | XCTAssert(opt2 === secondResult.command)
206 | XCTAssertTrue(secondResult.ignoreName)
207 | XCTAssertEqual(opt2.opt1, "value2")
208 |
209 | let (opt3, cli3, parser3) = setup()
210 |
211 | let thirdResult = try parser3.parse(cli: cli3, arguments: ArgumentList(arguments: []))
212 | XCTAssert(opt3 === thirdResult.command)
213 | XCTAssertTrue(thirdResult.ignoreName)
214 | XCTAssertNil(opt3.opt1)
215 | }
216 |
217 | }
218 |
219 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/StreamTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StreamTests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 4/2/18.
6 | //
7 |
8 | import XCTest
9 | import Dispatch
10 | @testable import SwiftCLI
11 |
12 | class StreamTests: XCTestCase {
13 |
14 | // MARK: - Write
15 |
16 | func testWrite() {
17 | let text = "first line\nsecond line"
18 |
19 | let pipe = Pipe()
20 | let write = WriteStream.for(fileHandle: pipe.fileHandleForWriting)
21 |
22 | write.write(text)
23 | write.closeWrite()
24 |
25 | let data = pipe.fileHandleForReading.readDataToEndOfFile()
26 | XCTAssertEqual(String(data: data, encoding: .utf8), text)
27 | }
28 |
29 | func testWriteData() {
30 | let data = "someString".data(using: .utf8)!
31 |
32 | let pipe = Pipe()
33 | let write = WriteStream.for(fileHandle: pipe.fileHandleForWriting)
34 | write.writeData(data)
35 | write.closeWrite()
36 |
37 | XCTAssertEqual(pipe.fileHandleForReading.readDataToEndOfFile(), data)
38 | }
39 |
40 | // MARK: - Read
41 |
42 | func testRead() {
43 | let pipe = Pipe()
44 | let read = ReadStream.for(fileHandle: pipe.fileHandleForReading)
45 |
46 | let first = "first line\n"
47 | pipe.fileHandleForWriting.write(first.data(using: .utf8)!)
48 | XCTAssertEqual(read.read(), first)
49 |
50 | let second = "second line\n"
51 | pipe.fileHandleForWriting.write(second.data(using: .utf8)!)
52 | XCTAssertEqual(read.read(), second)
53 | }
54 |
55 | func testReadData() {
56 | let data = "someString".data(using: .utf8)!
57 |
58 | let pipe = Pipe()
59 | let read = ReadStream.for(fileHandle: pipe.fileHandleForReading)
60 | pipe.fileHandleForWriting.write(data)
61 | XCTAssertEqual(read.readData(), data)
62 | }
63 |
64 | func testReadAll() {
65 | let pipe = PipeStream()
66 |
67 | pipe <<< "first line"
68 | pipe <<< "second line"
69 | pipe.closeWrite()
70 |
71 | XCTAssertEqual(pipe.readAll(), "first line\nsecond line\n")
72 | }
73 |
74 | func testReadLine() {
75 | let pipe = PipeStream()
76 |
77 | pipe <<< """
78 | first line
79 |
80 | second line
81 |
82 | """
83 | pipe.closeWrite()
84 |
85 | XCTAssertEqual(pipe.readLine(), "first line")
86 | XCTAssertEqual(pipe.readLine(), "")
87 | XCTAssertEqual(pipe.readLine(), "second line")
88 | XCTAssertEqual(pipe.readLine(), "")
89 | XCTAssertEqual(pipe.readLine(), nil)
90 |
91 | let pipe2 = PipeStream()
92 |
93 | pipe2.write("first ")
94 |
95 | DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(1)) {
96 | pipe2.write("line\nlast ")
97 | }
98 |
99 | DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(1500)) {
100 | pipe2.write("line")
101 | pipe2.closeWrite()
102 | }
103 |
104 | XCTAssertEqual(pipe2.readLine(), "first line")
105 | XCTAssertEqual(pipe2.readLine(), "last line")
106 | XCTAssertEqual(pipe2.readLine(), nil)
107 | }
108 |
109 | func testReadLines() {
110 | let pipe = PipeStream()
111 |
112 | pipe <<< """
113 | first line
114 |
115 | second line
116 |
117 | """
118 | pipe.closeWrite()
119 |
120 | XCTAssertEqual(Array(pipe.readLines()), ["first line", "", "second line", ""])
121 | }
122 |
123 | func testLineStream() {
124 | let firstLine = expectation(description: "first line")
125 | let stream = LineStream { (line) in
126 | if line == "first" {
127 | firstLine.fulfill()
128 | }
129 | }
130 |
131 | stream <<< "first"
132 | waitForExpectations(timeout: 1)
133 |
134 | stream.closeWrite()
135 | stream.waitToFinishProcessing()
136 | }
137 |
138 | func testCaptureStream() {
139 | let capture = CaptureStream()
140 |
141 | capture <<< "first"
142 | capture <<< ""
143 | capture <<< "second"
144 | capture.closeWrite()
145 |
146 | XCTAssertEqual(capture.readAll(), """
147 | first
148 |
149 | second
150 |
151 | """)
152 | }
153 |
154 | func testSplitStream() {
155 | let captureA = CaptureStream()
156 | let captureB = CaptureStream()
157 | let stream = SplitStream(captureA, captureB)
158 |
159 | stream <<< "first"
160 | stream <<< ""
161 | stream <<< "second"
162 | stream.closeWrite()
163 |
164 | XCTAssertEqual(captureA.readAll(), """
165 | first
166 |
167 | second
168 |
169 | """)
170 | XCTAssertEqual(captureB.readAll(), """
171 | first
172 |
173 | second
174 |
175 | """)
176 | }
177 |
178 | func testNullStream() {
179 | let nullWrite = WriteStream.null
180 |
181 | nullWrite <<< "into"
182 | nullWrite <<< "the"
183 | nullWrite <<< "void"
184 | }
185 |
186 | func testReadFile() {
187 | let stream = ReadStream.for(path: #file)
188 | XCTAssertEqual(stream?.readLine(), "//")
189 | XCTAssertEqual(stream?.readLine(), "// StreamTests.swift")
190 |
191 | stream?.seek(to: 0)
192 | XCTAssertEqual(stream?.readLine(), "//")
193 | XCTAssertEqual(stream?.readLine(), "// StreamTests.swift")
194 |
195 | stream?.seekToEnd()
196 | XCTAssertNil(stream?.readLine())
197 | }
198 |
199 | func testWriteFile() {
200 | let path = "/tmp/SwiftCLI.test"
201 | defer { try? FileManager.default.removeItem(atPath: path) }
202 |
203 | guard let stream = WriteStream.for(path: path) else {
204 | XCTFail()
205 | return
206 | }
207 |
208 | stream <<< "first line"
209 | stream <<< "second line"
210 |
211 | XCTAssertEqual(String(data: FileManager.default.contents(atPath: path)!, encoding: .utf8), """
212 | first line
213 | second line
214 |
215 | """)
216 |
217 | guard let secondStream = WriteStream.for(path: path, appending: false) else {
218 | XCTFail()
219 | return
220 | }
221 | secondStream.write("newww text")
222 |
223 | XCTAssertEqual(String(data: FileManager.default.contents(atPath: path)!, encoding: .utf8), """
224 | newww text
225 | second line
226 |
227 | """)
228 |
229 | secondStream.truncateRemaining()
230 |
231 | XCTAssertEqual(String(data: FileManager.default.contents(atPath: path)!, encoding: .utf8), """
232 | newww text
233 | """)
234 | }
235 |
236 | }
237 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/SwiftCLITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftCLITests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 8/2/14.
6 | // Copyright (c) 2014 jakeheis. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import SwiftCLI
11 |
12 | class SwiftCLITests: XCTestCase {
13 |
14 | private var executionString = ""
15 |
16 | // Integration tests
17 |
18 | func testGoWithArguments() {
19 | let (result, out, err) = runCLI { $0.go(with: ["test", "firstTest", "MyTester", "-t", "5", "-s"]) }
20 | XCTAssertEqual(result, 0, "Command should have succeeded")
21 | XCTAssertEqual(executionString, "MyTester will test firstTest, 5 times, silently", "Command should have produced accurate output")
22 | XCTAssertEqual(out, "")
23 | XCTAssertEqual(err, "")
24 | }
25 |
26 | func testCLIHelp() {
27 | let (result, out, err) = runCLI { $0.go(with: ["help"]) }
28 | XCTAssertEqual(result, 0, "Command should have succeeded")
29 | XCTAssertEqual(out, """
30 |
31 | Usage: tester [options]
32 |
33 | Commands:
34 | test A command to test stuff
35 | help Prints help information
36 |
37 |
38 | """)
39 | XCTAssertEqual(err, "")
40 |
41 | let (result2, out2, err2) = runCLI { $0.go(with: ["-h"]) }
42 | XCTAssertEqual(result2, 1, "Command should have failed")
43 | XCTAssertEqual(err2, """
44 |
45 | Usage: tester [options]
46 |
47 | Commands:
48 | test A command to test stuff
49 | help Prints help information
50 |
51 |
52 | """)
53 | XCTAssertEqual(out2, "")
54 | }
55 |
56 | func testGlobalOptions() {
57 | let verboseFlag = Flag("-v")
58 |
59 | let (result, out, err) = runCLI {
60 | $0.globalOptions.append(verboseFlag)
61 | return $0.go(with: ["test", "myTest", "-v"])
62 | }
63 | XCTAssertEqual(result, 0, "Command should have succeeded")
64 | XCTAssertEqual(executionString, "defaultTester will test myTest, 1 times", "Command should have produced accurate output")
65 | XCTAssertTrue(verboseFlag.wrappedValue)
66 | XCTAssertEqual(out, "")
67 | XCTAssertEqual(err, "")
68 | }
69 |
70 | func testOptionSplit() {
71 | let (result, out, err) = runCLI { $0.go(with: ["test", "firstTest", "MyTester", "-st", "5"]) }
72 | XCTAssertEqual(result, 0, "Command should have succeeded")
73 | XCTAssertEqual(executionString, "MyTester will test firstTest, 5 times, silently", "Command should have produced accurate output")
74 | XCTAssertEqual(out, "")
75 | XCTAssertEqual(err, "")
76 |
77 | executionString = ""
78 |
79 | let (result2, out2, err2) = runCLI { $0.go(with: ["test", "firstTest", "MyTester", "--times=5"]) }
80 | XCTAssertEqual(result2, 0, "Command should have succeeded")
81 | XCTAssertEqual(executionString, "MyTester will test firstTest, 5 times", "Command should have produced accurate output")
82 | XCTAssertEqual(out2, "")
83 | XCTAssertEqual(err2, "")
84 | }
85 |
86 | func testCommandHelp() {
87 | let (result, out, err) = runCLI { $0.go(with: ["test", "aTest", "-h"]) }
88 | XCTAssertEqual(result, 0)
89 | XCTAssertEqual(executionString, "")
90 | XCTAssertEqual(out, """
91 |
92 | Usage: tester test [] [options]
93 |
94 | A command to test stuff
95 |
96 | Options:
97 | -h, --help Show help information
98 | -s, --silent Silence all test output
99 | -t, --times Number of times to run the test
100 |
101 |
102 | """)
103 | XCTAssertEqual(err, "")
104 | }
105 |
106 | func testSingleCommand() {
107 | let cmd = RememberExecutionCmd()
108 | let cli = CLI(singleCommand: cmd)
109 |
110 | let (out, err) = CLI.capture {
111 | let result = cli.go(with: ["aTest"])
112 | XCTAssertEqual(result, 0)
113 | }
114 | XCTAssertEqual(out, "")
115 | XCTAssertEqual(err, "")
116 | XCTAssertTrue(cmd.executed)
117 | XCTAssertEqual(cmd.param, "aTest")
118 |
119 | let cmd2 = RememberExecutionCmd()
120 | let cli2 = CLI(singleCommand: cmd2)
121 |
122 | let (out2, err2) = CLI.capture {
123 | let result = cli2.go(with: [])
124 | XCTAssertEqual(result, 0)
125 | }
126 | XCTAssertEqual(out2, "")
127 | XCTAssertEqual(err2, "")
128 | XCTAssertTrue(cmd2.executed)
129 | XCTAssertNil(cmd2.param)
130 |
131 | let cmd3 = RememberExecutionCmd()
132 | let cli3 = CLI(singleCommand: cmd3)
133 |
134 | let (out3, err3) = CLI.capture {
135 | let result = cli3.go(with: ["-h"])
136 | XCTAssertEqual(result, 0)
137 | }
138 | XCTAssertEqual(out3, """
139 |
140 | Usage: cmd [] [options]
141 |
142 | Remembers execution
143 |
144 | Options:
145 | -h, --help Show help information
146 |
147 |
148 | """)
149 | XCTAssertEqual(err3, "")
150 | }
151 |
152 | func testFallback() {
153 | class Execute: Command {
154 | let name = "execute"
155 | @Param var file: String?
156 | var executed = false
157 | func execute() throws { executed = true }
158 | }
159 |
160 | class Build: Command {
161 | let name = "build"
162 | var built = false
163 | func execute() throws { built = true }
164 | }
165 |
166 | let execute1 = Execute()
167 | let build1 = Build()
168 |
169 | let (out, err) = CLI.capture {
170 | let cli = CLI(name: "swift", commands: [build1])
171 | cli.parser.routeBehavior = .searchWithFallback(execute1)
172 | let result = cli.go(with: ["build"])
173 | XCTAssertEqual(result, 0)
174 | }
175 | XCTAssertEqual(out, "")
176 | XCTAssertEqual(err, "")
177 | XCTAssertTrue(build1.built)
178 | XCTAssertFalse(execute1.executed)
179 | XCTAssertNil(execute1.file)
180 |
181 | let execute2 = Execute()
182 | let build2 = Build()
183 |
184 | let (out2, err2) = CLI.capture {
185 | let cli = CLI(name: "swift", commands: [build2])
186 | cli.parser.routeBehavior = .searchWithFallback(execute2)
187 | let result = cli.go(with: ["file.swift"])
188 | XCTAssertEqual(result, 0)
189 | }
190 | XCTAssertEqual(out2, "")
191 | XCTAssertEqual(err2, "")
192 | XCTAssertFalse(build2.built)
193 | XCTAssertTrue(execute2.executed)
194 | XCTAssertEqual(execute2.file, "file.swift")
195 |
196 | let execute3 = Execute()
197 | let build3 = Build()
198 |
199 | let (out3, err3) = CLI.capture {
200 | let cli = CLI(name: "swift", commands: [build3])
201 | cli.parser.routeBehavior = .searchWithFallback(execute3)
202 | let result = cli.go(with: [])
203 | XCTAssertEqual(result, 0)
204 | }
205 | XCTAssertEqual(out3, "")
206 | XCTAssertEqual(err3, "")
207 | XCTAssertFalse(build3.built)
208 | XCTAssertTrue(execute3.executed)
209 | XCTAssertNil(execute3.file)
210 |
211 | let execute4 = Execute()
212 | let build4 = Build()
213 |
214 | let (out4, err4) = CLI.capture {
215 | let cli = CLI(name: "swift", commands: [build4])
216 | cli.parser.routeBehavior = .searchWithFallback(execute4)
217 | let result = cli.go(with: ["-h"])
218 | XCTAssertEqual(result, 0)
219 | }
220 | XCTAssertEqual(out4, """
221 |
222 | Usage: swift [] [options]
223 |
224 | Options:
225 | -h, --help Show help information
226 |
227 |
228 | """)
229 | XCTAssertEqual(err4, "")
230 |
231 | let execute5 = Execute()
232 | let build5 = Build()
233 |
234 | let (out5, err5) = CLI.capture {
235 | let cli = CLI(name: "swift", commands: [build5])
236 | cli.parser.routeBehavior = .searchWithFallback(execute5)
237 | let result = cli.go(with: ["hi.swift", "this.swift"])
238 | XCTAssertEqual(result, 1)
239 | }
240 | XCTAssertEqual(err5, """
241 |
242 | Usage: swift [] [options]
243 |
244 | Options:
245 | -h, --help Show help information
246 |
247 | Error: command requires between 0 and 1 arguments
248 |
249 |
250 | """)
251 | XCTAssertEqual(out5, "")
252 | }
253 |
254 | private func runCLI(_ run: (CLI) -> Int32) -> (Int32, String, String) {
255 | let cmd = TestCommand { (executionString) in
256 | self.executionString = executionString
257 | }
258 |
259 | var result: Int32 = 0
260 | let (out, err) = CLI.capture {
261 | let cli = CLI.createTester(commands: [cmd])
262 | result = run(cli)
263 | }
264 |
265 | return (result, out, err)
266 | }
267 |
268 | // Tear down
269 |
270 | override func tearDown() {
271 | super.tearDown()
272 |
273 | executionString = ""
274 | }
275 |
276 | }
277 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/TaskTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TaskTests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 4/1/18.
6 | //
7 |
8 | import Foundation
9 | import SwiftCLI
10 | import XCTest
11 |
12 | class TaskTests: XCTestCase {
13 | #if !os(iOS)
14 |
15 | func testRun() throws {
16 | let file = "file.txt"
17 | try Task.run("/usr/bin/touch", file)
18 |
19 | XCTAssertTrue(FileManager.default.fileExists(atPath: file))
20 | try FileManager.default.removeItem(atPath: file)
21 | }
22 |
23 | func testCapture() throws {
24 | let path = "/tmp/_swiftcli"
25 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
26 | _ = FileManager.default.createFile(atPath: path + "/SwiftCLI", contents: nil, attributes: nil)
27 | defer { try! FileManager.default.removeItem(atPath: path) }
28 |
29 | let output = try Task.capture("/bin/ls", path)
30 | XCTAssertEqual(output.stdout, "SwiftCLI")
31 | XCTAssertEqual(output.stderr, "")
32 | }
33 |
34 | func testBashRun() throws {
35 | let file = "file.txt"
36 | try Task.run(bash: "touch \(file)")
37 |
38 | XCTAssertTrue(FileManager.default.fileExists(atPath: file))
39 | try FileManager.default.removeItem(atPath: file)
40 | }
41 |
42 | func testBashCapture() throws {
43 | let path = "/tmp/_swiftcli"
44 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
45 | _ = FileManager.default.createFile(atPath: path + "/SwiftCLI", contents: nil, attributes: nil)
46 | defer { try! FileManager.default.removeItem(atPath: path) }
47 |
48 | let output = try Task.capture(bash: "ls \(path)")
49 | XCTAssertEqual(output.stdout, "SwiftCLI")
50 | XCTAssertEqual(output.stderr, "")
51 | }
52 |
53 | func testRunDirectory() throws {
54 | let path = "/tmp/_swiftcli"
55 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
56 | defer { try! FileManager.default.removeItem(atPath: path) }
57 |
58 | try Task.run("touch", arguments: ["SwiftCLI"], directory: path)
59 |
60 | XCTAssertTrue(FileManager.default.fileExists(atPath: path + "/SwiftCLI"))
61 | }
62 |
63 | func testCaptureDirectory() throws {
64 | let path = "/tmp/_swiftcli"
65 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
66 | _ = FileManager.default.createFile(atPath: path + "/SwiftCLI", contents: nil, attributes: nil)
67 | defer { try! FileManager.default.removeItem(atPath: path) }
68 |
69 | let output = try Task.capture("ls", arguments: [], directory: path)
70 | XCTAssertEqual(output.stdout, "SwiftCLI")
71 | XCTAssertEqual(output.stderr, "")
72 | }
73 |
74 | func testIn() throws {
75 | let input = PipeStream()
76 |
77 | let output = CaptureStream()
78 | let task = Task(executable: "/usr/bin/sort", stdout: output, stdin: input)
79 | task.runAsync()
80 |
81 | input <<< "beta"
82 | input <<< "alpha"
83 | input.closeWrite()
84 |
85 | let code = task.finish()
86 | XCTAssertEqual(code, 0)
87 | XCTAssertEqual(output.readAll(), "alpha\nbeta\n")
88 | }
89 |
90 | func testPipe() throws {
91 | // Travis errors on Linux for unknown reason
92 | #if os(macOS)
93 |
94 | let path = "/tmp/_swiftcli"
95 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
96 | _ = FileManager.default.createFile(atPath: path + "/Info.plist", contents: nil, attributes: nil)
97 | _ = FileManager.default.createFile(atPath: path + "/LinuxMain.swift", contents: nil, attributes: nil)
98 | _ = FileManager.default.createFile(atPath: path + "/SwiftCLITests", contents: nil, attributes: nil)
99 | defer { try! FileManager.default.removeItem(atPath: path) }
100 |
101 | let connector = PipeStream()
102 | let output = CaptureStream()
103 |
104 | let ls = Task(executable: "ls", arguments: [path], stdout: connector)
105 | let grep = Task(executable: "grep", arguments: ["Swift"], stdout: output, stdin: connector)
106 |
107 | ls.runAsync()
108 | grep.runAsync()
109 |
110 | XCTAssertEqual(output.readAll(), "SwiftCLITests\n")
111 |
112 | #endif
113 | }
114 |
115 | func testCurrentDirectory() throws {
116 | let path = "/tmp/_swiftcli"
117 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
118 | _ = FileManager.default.createFile(atPath: path + "/SwiftCLI", contents: nil, attributes: nil)
119 | defer { try! FileManager.default.removeItem(atPath: path) }
120 |
121 | let capture = CaptureStream()
122 |
123 | let ls = Task(executable: "ls", directory: path, stdout: capture)
124 | ls.runSync()
125 |
126 | XCTAssertEqual(capture.readAll(), "SwiftCLI\n")
127 | }
128 |
129 | func testEnv() {
130 | let capture = CaptureStream()
131 |
132 | let echo = Task(executable: "bash", arguments: ["-c", "echo $MY_VAR"], stdout: capture)
133 | echo.env["MY_VAR"] = "aVal"
134 | echo.runSync()
135 |
136 | XCTAssertEqual(capture.readAll(), "aVal\n")
137 | }
138 |
139 | func testSignals() {
140 | let task = Task(executable: "/bin/sleep", arguments: ["1"])
141 | task.runAsync()
142 |
143 | XCTAssertTrue(task.suspend())
144 | sleep(2)
145 | XCTAssertTrue(task.isRunning)
146 | XCTAssertTrue(task.resume())
147 | sleep(2)
148 | XCTAssertFalse(task.isRunning)
149 |
150 | // Travis errors when calling interrupt on Linux for unknown reason
151 | #if os(macOS)
152 | let task2 = Task(executable: "/bin/sleep", arguments: ["3"])
153 | task2.runAsync()
154 | task2.interrupt()
155 | XCTAssertEqual(task2.finish(), 2)
156 | #endif
157 |
158 | let task3 = Task(executable: "/bin/sleep", arguments: ["3"])
159 | task3.runAsync()
160 | task3.terminate()
161 | XCTAssertEqual(task3.finish(), 15)
162 | }
163 |
164 | func testTaskLineStream() throws {
165 | let path = "/tmp/_swiftcli"
166 | try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: false, attributes: nil)
167 | _ = FileManager.default.createFile(atPath: path + "/Info.plist", contents: nil, attributes: nil)
168 | _ = FileManager.default.createFile(atPath: path + "/LinuxMain.swift", contents: nil, attributes: nil)
169 | _ = FileManager.default.createFile(atPath: path + "/SwiftCLITests", contents: nil, attributes: nil)
170 | defer { try! FileManager.default.removeItem(atPath: path) }
171 |
172 | var count = 0
173 | let lineStream = LineStream { (line) in
174 | count += 1
175 | }
176 | let task = Task(executable: "ls", arguments: [path], stdout: lineStream)
177 | XCTAssertEqual(task.runSync(), 0)
178 |
179 | XCTAssertEqual(count, 3)
180 | }
181 |
182 | func testTaskNullStream() throws {
183 | let task = Task(executable: "ls", stdout: WriteStream.null)
184 | task.runSync()
185 | }
186 |
187 | #endif
188 | }
189 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/ValidationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ValidationTests.swift
3 | // SwiftCLITests
4 | //
5 | // Created by Jake Heiser on 11/23/18.
6 | //
7 |
8 | import XCTest
9 | import SwiftCLI
10 |
11 | class ValidationTests: XCTestCase {
12 |
13 | func testEquatable() {
14 | let allow = Validation.allowing("this", "that")
15 | assertFailure(of: allow, with: "something", message: "must be one of: this, that")
16 | assertSuccess(of: allow, with: "this")
17 | assertSuccess(of: allow, with: "that")
18 |
19 | let reject = Validation.rejecting("this", "that")
20 | assertFailure(of: reject, with: "this", message: "must not be: this, that")
21 | assertFailure(of: reject, with: "that", message: "must not be: this, that")
22 | assertSuccess(of: reject, with: "something")
23 | }
24 |
25 | func testComparable() {
26 | let greaterThan = Validation.greaterThan(18)
27 | assertFailure(of: greaterThan, with: 15, message: "must be greater than 18")
28 | assertSuccess(of: greaterThan, with: 19)
29 |
30 | let lessThan = Validation.lessThan(18)
31 | assertFailure(of: lessThan, with: 19, message: "must be less than 18")
32 | assertSuccess(of: lessThan, with: 15)
33 |
34 | let withinClosed = Validation.within(18...30)
35 | assertFailure(of: withinClosed, with: 15, message: "must be greater than or equal to 18 and less than or equal to 30")
36 | assertFailure(of: withinClosed, with: 31, message: "must be greater than or equal to 18 and less than or equal to 30")
37 | assertSuccess(of: withinClosed, with: 18)
38 | assertSuccess(of: withinClosed, with: 24)
39 | assertSuccess(of: withinClosed, with: 30)
40 |
41 | let withinHalfOpen = Validation.within(18..<30)
42 | assertFailure(of: withinHalfOpen, with: 15, message: "must be greater than or equal to 18 and less than 30")
43 | assertFailure(of: withinHalfOpen, with: 30, message: "must be greater than or equal to 18 and less than 30")
44 | assertFailure(of: withinHalfOpen, with: 31, message: "must be greater than or equal to 18 and less than 30")
45 | assertSuccess(of: withinHalfOpen, with: 18)
46 | assertSuccess(of: withinHalfOpen, with: 24)
47 | }
48 |
49 | func testString() {
50 | let contains = Validation.contains("hi")
51 | assertFailure(of: contains, with: "that", message: "must contain 'hi'")
52 | assertSuccess(of: contains, with: "this")
53 | }
54 |
55 | private func assertSuccess(of validation: Validation, with input: T, file: StaticString = #file, line: UInt = #line) {
56 | XCTAssertTrue(validation.validate(input), file: file, line: line)
57 | }
58 |
59 | private func assertFailure(of validation: Validation, with input: T, message expectedMessage: String, file: StaticString = #file, line: UInt = #line) {
60 | XCTAssertFalse(validation.validate(input), file: file, line: line)
61 | XCTAssertEqual(validation.message, expectedMessage, file: file, line: line)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Tests/SwiftCLITests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | #if !canImport(ObjectiveC)
2 | import XCTest
3 |
4 | extension ArgumentListTests {
5 | // DO NOT MODIFY: This is autogenerated, use:
6 | // `swift test --generate-linuxmain`
7 | // to regenerate.
8 | static let __allTests__ArgumentListTests = [
9 | ("testManipulate", testManipulate),
10 | ]
11 | }
12 |
13 | extension CompletionGeneratorTests {
14 | // DO NOT MODIFY: This is autogenerated, use:
15 | // `swift test --generate-linuxmain`
16 | // to regenerate.
17 | static let __allTests__CompletionGeneratorTests = [
18 | ("testBasicOptions", testBasicOptions),
19 | ("testEscaping", testEscaping),
20 | ("testFunction", testFunction),
21 | ("testGroup", testGroup),
22 | ("testLayered", testLayered),
23 | ("testOptionCompletion", testOptionCompletion),
24 | ("testParameterCompletion", testParameterCompletion),
25 | ("testSepcialCaseOptionCompletion", testSepcialCaseOptionCompletion),
26 | ]
27 | }
28 |
29 | extension HelpMessageGeneratorTests {
30 | // DO NOT MODIFY: This is autogenerated, use:
31 | // `swift test --generate-linuxmain`
32 | // to regenerate.
33 | static let __allTests__HelpMessageGeneratorTests = [
34 | ("testColoredError", testColoredError),
35 | ("testCommandListGeneration", testCommandListGeneration),
36 | ("testCommandNotFound", testCommandNotFound),
37 | ("testCommandNotSpecified", testCommandNotSpecified),
38 | ("testExpectedValueAfterKey", testExpectedValueAfterKey),
39 | ("testIllegalOptionType", testIllegalOptionType),
40 | ("testInheritedUsageStatementGeneration", testInheritedUsageStatementGeneration),
41 | ("testInvalidOptionValue", testInvalidOptionValue),
42 | ("testInvalidParameterValue", testInvalidParameterValue),
43 | ("testLongDescriptionGeneration", testLongDescriptionGeneration),
44 | ("testMisusedOptionsStatementGeneration", testMisusedOptionsStatementGeneration),
45 | ("testMutlineCommandListGeneration", testMutlineCommandListGeneration),
46 | ("testMutlineUsageStatementGeneration", testMutlineUsageStatementGeneration),
47 | ("testNoCommandMisusedOption", testNoCommandMisusedOption),
48 | ("testOptionGroupMisuse", testOptionGroupMisuse),
49 | ("testParameterCountError", testParameterCountError),
50 | ("testParameterTypeError", testParameterTypeError),
51 | ("testReversedOrderUsageStatementGeneration", testReversedOrderUsageStatementGeneration),
52 | ("testUsageStatementGeneration", testUsageStatementGeneration),
53 | ]
54 | }
55 |
56 | extension InputTests {
57 | // DO NOT MODIFY: This is autogenerated, use:
58 | // `swift test --generate-linuxmain`
59 | // to regenerate.
60 | static let __allTests__InputTests = [
61 | ("testBool", testBool),
62 | ("testDouble", testDouble),
63 | ("testInt", testInt),
64 | ("testValidation", testValidation),
65 | ]
66 | }
67 |
68 | extension OptionRegistryTests {
69 | // DO NOT MODIFY: This is autogenerated, use:
70 | // `swift test --generate-linuxmain`
71 | // to regenerate.
72 | static let __allTests__OptionRegistryTests = [
73 | ("testFlagDetection", testFlagDetection),
74 | ("testKeyDetection", testKeyDetection),
75 | ("testMultipleRestrictions", testMultipleRestrictions),
76 | ("testVariadicDetection", testVariadicDetection),
77 | ]
78 | }
79 |
80 | extension ParameterFillerTests {
81 | // DO NOT MODIFY: This is autogenerated, use:
82 | // `swift test --generate-linuxmain`
83 | // to regenerate.
84 | static let __allTests__ParameterFillerTests = [
85 | ("testCollectedOptionalParameters", testCollectedOptionalParameters),
86 | ("testCollectedRequiredParameters", testCollectedRequiredParameters),
87 | ("testCombinedRequiredAndOptionalParameters", testCombinedRequiredAndOptionalParameters),
88 | ("testCustomParameter", testCustomParameter),
89 | ("testEmptyOptionalCollectedParameter", testEmptyOptionalCollectedParameter),
90 | ("testEmptySignature", testEmptySignature),
91 | ("testOptionalParameters", testOptionalParameters),
92 | ("testOptionalParametersWithInheritance", testOptionalParametersWithInheritance),
93 | ("testParameterInit", testParameterInit),
94 | ("testRequiredParameters", testRequiredParameters),
95 | ("testValidatedParameter", testValidatedParameter),
96 | ]
97 | }
98 |
99 | extension ParserTests {
100 | // DO NOT MODIFY: This is autogenerated, use:
101 | // `swift test --generate-linuxmain`
102 | // to regenerate.
103 | static let __allTests__ParserTests = [
104 | ("testBeforeCommand", testBeforeCommand),
105 | ("testCollectedOptions", testCollectedOptions),
106 | ("testCombinedFlagsAndKeysAndArgumentsParsing", testCombinedFlagsAndKeysAndArgumentsParsing),
107 | ("testCombinedFlagsAndKeysParsing", testCombinedFlagsAndKeysParsing),
108 | ("testCounterParse", testCounterParse),
109 | ("testFlagGivenValue", testFlagGivenValue),
110 | ("testFlagSplitting", testFlagSplitting),
111 | ("testFullParse", testFullParse),
112 | ("testGroupRestriction", testGroupRestriction),
113 | ("testIllegalOptionFormat", testIllegalOptionFormat),
114 | ("testKeysNotGivenValues", testKeysNotGivenValues),
115 | ("testKeyValueParsing", testKeyValueParsing),
116 | ("testSimpleFlagParsing", testSimpleFlagParsing),
117 | ("testSimpleKeyParsing", testSimpleKeyParsing),
118 | ("testUnrecognizedOptions", testUnrecognizedOptions),
119 | ("testValidation", testValidation),
120 | ("testVaridadicParse", testVaridadicParse),
121 | ]
122 | }
123 |
124 | extension RouterTests {
125 | // DO NOT MODIFY: This is autogenerated, use:
126 | // `swift test --generate-linuxmain`
127 | // to regenerate.
128 | static let __allTests__RouterTests = [
129 | ("testAliasRoute", testAliasRoute),
130 | ("testFailedRoute", testFailedRoute),
131 | ("testFallback", testFallback),
132 | ("testFallbackOption", testFallbackOption),
133 | ("testGroupFailedRoute", testGroupFailedRoute),
134 | ("testGroupPartialRoute", testGroupPartialRoute),
135 | ("testGroupSuccessRoute", testGroupSuccessRoute),
136 | ("testNameRoute", testNameRoute),
137 | ("testNestedGroup", testNestedGroup),
138 | ("testSingleRouter", testSingleRouter),
139 | ]
140 | }
141 |
142 | extension StreamTests {
143 | // DO NOT MODIFY: This is autogenerated, use:
144 | // `swift test --generate-linuxmain`
145 | // to regenerate.
146 | static let __allTests__StreamTests = [
147 | ("testCaptureStream", testCaptureStream),
148 | ("testLineStream", testLineStream),
149 | ("testNullStream", testNullStream),
150 | ("testRead", testRead),
151 | ("testReadAll", testReadAll),
152 | ("testReadData", testReadData),
153 | ("testReadFile", testReadFile),
154 | ("testReadLine", testReadLine),
155 | ("testReadLines", testReadLines),
156 | ("testWrite", testWrite),
157 | ("testWriteData", testWriteData),
158 | ("testWriteFile", testWriteFile),
159 | ]
160 | }
161 |
162 | extension SwiftCLITests {
163 | // DO NOT MODIFY: This is autogenerated, use:
164 | // `swift test --generate-linuxmain`
165 | // to regenerate.
166 | static let __allTests__SwiftCLITests = [
167 | ("testCLIHelp", testCLIHelp),
168 | ("testCommandHelp", testCommandHelp),
169 | ("testFallback", testFallback),
170 | ("testGlobalOptions", testGlobalOptions),
171 | ("testGoWithArguments", testGoWithArguments),
172 | ("testOptionSplit", testOptionSplit),
173 | ("testSingleCommand", testSingleCommand),
174 | ]
175 | }
176 |
177 | extension TaskTests {
178 | // DO NOT MODIFY: This is autogenerated, use:
179 | // `swift test --generate-linuxmain`
180 | // to regenerate.
181 | static let __allTests__TaskTests = [
182 | ("testBashCapture", testBashCapture),
183 | ("testBashRun", testBashRun),
184 | ("testCapture", testCapture),
185 | ("testCaptureDirectory", testCaptureDirectory),
186 | ("testCurrentDirectory", testCurrentDirectory),
187 | ("testEnv", testEnv),
188 | ("testIn", testIn),
189 | ("testPipe", testPipe),
190 | ("testRun", testRun),
191 | ("testRunDirectory", testRunDirectory),
192 | ("testSignals", testSignals),
193 | ("testTaskLineStream", testTaskLineStream),
194 | ("testTaskNullStream", testTaskNullStream),
195 | ]
196 | }
197 |
198 | extension ValidationTests {
199 | // DO NOT MODIFY: This is autogenerated, use:
200 | // `swift test --generate-linuxmain`
201 | // to regenerate.
202 | static let __allTests__ValidationTests = [
203 | ("testComparable", testComparable),
204 | ("testEquatable", testEquatable),
205 | ("testString", testString),
206 | ]
207 | }
208 |
209 | public func __allTests() -> [XCTestCaseEntry] {
210 | return [
211 | testCase(ArgumentListTests.__allTests__ArgumentListTests),
212 | testCase(CompletionGeneratorTests.__allTests__CompletionGeneratorTests),
213 | testCase(HelpMessageGeneratorTests.__allTests__HelpMessageGeneratorTests),
214 | testCase(InputTests.__allTests__InputTests),
215 | testCase(OptionRegistryTests.__allTests__OptionRegistryTests),
216 | testCase(ParameterFillerTests.__allTests__ParameterFillerTests),
217 | testCase(ParserTests.__allTests__ParserTests),
218 | testCase(RouterTests.__allTests__RouterTests),
219 | testCase(StreamTests.__allTests__StreamTests),
220 | testCase(SwiftCLITests.__allTests__SwiftCLITests),
221 | testCase(TaskTests.__allTests__TaskTests),
222 | testCase(ValidationTests.__allTests__ValidationTests),
223 | ]
224 | }
225 | #endif
226 |
--------------------------------------------------------------------------------