diff --git a/.gitignore b/.gitignore index 8022289..64661b1 100644 --- a/.gitignore +++ b/.gitignore @@ -70,7 +70,6 @@ Network Trash Folder Temporary Items .apdisk -Seeds/ mas-cli.zip mas-cli.dSYM.zip mas.xcarchive.zip diff --git a/.travis.yml b/.travis.yml index 83e2fa9..2a641c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,9 +8,6 @@ env: - LC_ALL=en_US.UTF-8 - LANGUAGE=en_US.UTF-8 -install: - - script/bootstrap - script: - script/build diff --git a/Seeds/Commandant/Sources/Commandant/Argument.swift b/Seeds/Commandant/Sources/Commandant/Argument.swift new file mode 100644 index 0000000..9c7d293 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/Argument.swift @@ -0,0 +1,95 @@ +// +// Argument.swift +// Commandant +// +// Created by Syo Ikeda on 12/14/15. +// Copyright (c) 2015 Carthage. All rights reserved. +// + + +/// Describes an argument that can be provided on the command line. +public struct Argument { + /// The default value for this argument. This is the value that will be used + /// if the argument is never explicitly specified on the command line. + /// + /// If this is nil, this argument is always required. + public let defaultValue: T? + + /// A human-readable string describing the purpose of this argument. This will + /// be shown in help messages. + public let usage: String + + public init(defaultValue: T? = nil, usage: String) { + self.defaultValue = defaultValue + self.usage = usage + } + + fileprivate func invalidUsageError(_ value: String) -> CommandantError { + let description = "Invalid value for '\(self)': \(value)" + return .usageError(description: description) + } +} + +/// Evaluates the given argument in the given mode. +/// +/// If parsing command line arguments, and no value was specified on the command +/// line, the argument's `defaultValue` is used. +public func <| (mode: CommandMode, argument: Argument) -> Result> { + switch mode { + case let .arguments(arguments): + guard let stringValue = arguments.consumePositionalArgument() else { + if let defaultValue = argument.defaultValue { + return .success(defaultValue) + } else { + return .failure(missingArgumentError(argument.usage)) + } + } + + if let value = T.from(string: stringValue) { + return .success(value) + } else { + return .failure(argument.invalidUsageError(stringValue)) + } + + case .usage: + return .failure(informativeUsageError(argument)) + } +} + +/// Evaluates the given argument list in the given mode. +/// +/// If parsing command line arguments, and no value was specified on the command +/// line, the argument's `defaultValue` is used. +public func <| (mode: CommandMode, argument: Argument<[T]>) -> Result<[T], CommandantError> { + switch mode { + case let .arguments(arguments): + guard let firstValue = arguments.consumePositionalArgument() else { + if let defaultValue = argument.defaultValue { + return .success(defaultValue) + } else { + return .failure(missingArgumentError(argument.usage)) + } + } + + var values = [T]() + + guard let value = T.from(string: firstValue) else { + return .failure(argument.invalidUsageError(firstValue)) + } + + values.append(value) + + while let nextValue = arguments.consumePositionalArgument() { + guard let value = T.from(string: nextValue) else { + return .failure(argument.invalidUsageError(nextValue)) + } + + values.append(value) + } + + return .success(values) + + case .usage: + return .failure(informativeUsageError(argument)) + } +} diff --git a/Seeds/Commandant/Sources/Commandant/ArgumentParser.swift b/Seeds/Commandant/Sources/Commandant/ArgumentParser.swift new file mode 100644 index 0000000..5eaec41 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/ArgumentParser.swift @@ -0,0 +1,191 @@ +// +// ArgumentParser.swift +// Commandant +// +// Created by Justin Spahr-Summers on 2014-11-21. +// Copyright (c) 2014 Carthage. All rights reserved. +// + +import Foundation + +/// Represents an argument passed on the command line. +private enum RawArgument: Equatable { + /// A key corresponding to an option (e.g., `verbose` for `--verbose`). + case key(String) + + /// A value, either associated with an option or passed as a positional + /// argument. + case value(String) + + /// One or more flag arguments (e.g 'r' and 'f' for `-rf`) + case flag(Set) +} + +private func ==(lhs: RawArgument, rhs: RawArgument) -> Bool { + switch (lhs, rhs) { + case let (.key(left), .key(right)): + return left == right + + case let (.value(left), .value(right)): + return left == right + + case let (.flag(left), .flag(right)): + return left == right + + default: + return false + } +} + +extension RawArgument: CustomStringConvertible { + fileprivate var description: String { + switch self { + case let .key(key): + return "--\(key)" + + case let .value(value): + return "\"\(value)\"" + + case let .flag(flags): + return "-\(String(flags))" + } + } +} + +/// Destructively parses a list of command-line arguments. +public final class ArgumentParser { + /// The remaining arguments to be extracted, in their raw form. + private var rawArguments: [RawArgument] = [] + + /// Initializes the generator from a simple list of command-line arguments. + public init(_ arguments: [String]) { + // The first instance of `--` terminates the option list. + let params = arguments.split(maxSplits: 1, omittingEmptySubsequences: false) { $0 == "--" } + + // Parse out the keyed and flag options. + let options = params.first! + rawArguments.append(contentsOf: options.map { arg in + if arg.hasPrefix("-") { + // Do we have `--{key}` or `-{flags}`. + let opt = arg.characters.dropFirst() + if opt.first == "-" { + return .key(String(opt.dropFirst())) + } else { + return .flag(Set(opt)) + } + } else { + return .value(arg) + } + }) + + // Remaining arguments are all positional parameters. + if params.count == 2 { + let positional = params.last! + rawArguments.append(contentsOf: positional.map(RawArgument.value)) + } + } + + /// Returns the remaining arguments. + internal var remainingArguments: [String]? { + return rawArguments.isEmpty ? nil : rawArguments.map { $0.description } + } + + /// Returns whether the given key was enabled or disabled, or nil if it + /// was not given at all. + /// + /// If the key is found, it is then removed from the list of arguments + /// remaining to be parsed. + internal func consumeBoolean(forKey key: String) -> Bool? { + let oldArguments = rawArguments + rawArguments.removeAll() + + var result: Bool? + for arg in oldArguments { + if arg == .key(key) { + result = true + } else if arg == .key("no-\(key)") { + result = false + } else { + rawArguments.append(arg) + } + } + + return result + } + + /// Returns the value associated with the given flag, or nil if the flag was + /// not specified. If the key is presented, but no value was given, an error + /// is returned. + /// + /// If a value is found, the key and the value are both removed from the + /// list of arguments remaining to be parsed. + internal func consumeValue(forKey key: String) -> Result> { + let oldArguments = rawArguments + rawArguments.removeAll() + + var foundValue: String? + var index = 0 + + while index < oldArguments.count { + defer { index += 1 } + let arg = oldArguments[index] + + guard arg == .key(key) else { + rawArguments.append(arg) + continue + } + + index += 1 + guard index < oldArguments.count, case let .value(value) = oldArguments[index] else { + return .failure(missingArgumentError("--\(key)")) + } + + foundValue = value + } + + return .success(foundValue) + } + + /// Returns the next positional argument that hasn't yet been returned, or + /// nil if there are no more positional arguments. + internal func consumePositionalArgument() -> String? { + for (index, arg) in rawArguments.enumerated() { + if case let .value(value) = arg { + rawArguments.remove(at: index) + return value + } + } + + return nil + } + + /// Returns whether the given key was specified and removes it from the + /// list of arguments remaining. + internal func consume(key: String) -> Bool { + let oldArguments = rawArguments + rawArguments = oldArguments.filter { $0 != .key(key) } + + return rawArguments.count < oldArguments.count + } + + /// Returns whether the given flag was specified and removes it from the + /// list of arguments remaining. + internal func consumeBoolean(flag: Character) -> Bool { + for (index, arg) in rawArguments.enumerated() { + if case let .flag(flags) = arg, flags.contains(flag) { + var flags = flags + flags.remove(flag) + + if flags.isEmpty { + rawArguments.remove(at: index) + } else { + rawArguments[index] = .flag(flags) + } + + return true + } + } + + return false + } +} diff --git a/Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift b/Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift new file mode 100644 index 0000000..2fab660 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift @@ -0,0 +1,41 @@ +// +// ArgumentProtocol.swift +// Commandant +// +// Created by Syo Ikeda on 12/14/15. +// Copyright (c) 2015 Carthage. All rights reserved. +// + +/// Represents a value that can be converted from a command-line argument. +public protocol ArgumentProtocol { + /// A human-readable name for this type. + static var name: String { get } + + /// Attempts to parse a value from the given command-line argument. + static func from(string: String) -> Self? +} + +extension Int: ArgumentProtocol { + public static let name = "integer" + + public static func from(string: String) -> Int? { + return Int(string) + } +} + +extension String: ArgumentProtocol { + public static let name = "string" + + public static func from(string: String) -> String? { + return string + } +} + +// MARK: - migration support +@available(*, unavailable, renamed: "ArgumentProtocol") +public typealias ArgumentType = ArgumentProtocol + +extension ArgumentProtocol { + @available(*, unavailable, renamed: "from(string:)") + static func fromString(_ string: String) -> Self? { return nil } +} diff --git a/Seeds/Commandant/Sources/Commandant/Command.swift b/Seeds/Commandant/Sources/Commandant/Command.swift new file mode 100644 index 0000000..538c72b --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/Command.swift @@ -0,0 +1,224 @@ +// +// Command.swift +// Commandant +// +// Created by Justin Spahr-Summers on 2014-10-10. +// Copyright (c) 2014 Carthage. All rights reserved. +// + +import Foundation + +/// Represents a subcommand that can be executed with its own set of arguments. +public protocol CommandProtocol { + + /// The command's options type. + associatedtype Options: OptionsProtocol + + associatedtype ClientError: Error = Options.ClientError + + /// The action that users should specify to use this subcommand (e.g., + /// `help`). + var verb: String { get } + + /// A human-readable, high-level description of what this command is used + /// for. + var function: String { get } + + /// Runs this subcommand with the given options. + func run(_ options: Options) -> Result<(), ClientError> +} + +/// A type-erased command. +public struct CommandWrapper { + public let verb: String + public let function: String + + public let run: (ArgumentParser) -> Result<(), CommandantError> + + public let usage: () -> CommandantError? + + /// Creates a command that wraps another. + fileprivate init(_ command: C) where C.ClientError == ClientError, C.Options.ClientError == ClientError { + verb = command.verb + function = command.function + run = { (arguments: ArgumentParser) -> Result<(), CommandantError> in + let options = C.Options.evaluate(.arguments(arguments)) + + if let remainingArguments = arguments.remainingArguments { + return .failure(unrecognizedArgumentsError(remainingArguments)) + } + + switch options { + case let .success(options): + return command + .run(options) + .mapError(CommandantError.commandError) + + case let .failure(error): + return .failure(error) + } + } + usage = { () -> CommandantError? in + return C.Options.evaluate(.usage).error + } + } +} + +/// Describes the "mode" in which a command should run. +public enum CommandMode { + /// Options should be parsed from the given command-line arguments. + case arguments(ArgumentParser) + + /// Each option should record its usage information in an error, for + /// presentation to the user. + case usage +} + +/// Maintains the list of commands available to run. +public final class CommandRegistry { + private var commandsByVerb: [String: CommandWrapper] = [:] + + /// All available commands. + public var commands: [CommandWrapper] { + return commandsByVerb.values.sorted { return $0.verb < $1.verb } + } + + public init() {} + + /// Registers the given command, making it available to run. + /// + /// If another command was already registered with the same `verb`, it will + /// be overwritten. + public func register(_ command: C) where C.ClientError == ClientError, C.Options.ClientError == ClientError { + commandsByVerb[command.verb] = CommandWrapper(command) + } + + /// Runs the command corresponding to the given verb, passing it the given + /// arguments. + /// + /// Returns the results of the execution, or nil if no such command exists. + public func run(command verb: String, arguments: [String]) -> Result<(), CommandantError>? { + return self[verb]?.run(ArgumentParser(arguments)) + } + + /// Returns the command matching the given verb, or nil if no such command + /// is registered. + public subscript(verb: String) -> CommandWrapper? { + return commandsByVerb[verb] + } +} + +extension CommandRegistry { + /// Hands off execution to the CommandRegistry, by parsing CommandLine.arguments + /// and then running whichever command has been identified in the argument + /// list. + /// + /// If the chosen command executes successfully, the process will exit with + /// a successful exit code. + /// + /// If the chosen command fails, the provided error handler will be invoked, + /// then the process will exit with a failure exit code. + /// + /// If a matching command could not be found but there is any `executable-verb` + /// style subcommand executable in the caller's `$PATH`, the subcommand will + /// be executed. + /// + /// If a matching command could not be found or a usage error occurred, + /// a helpful error message will be written to `stderr`, then the process + /// will exit with a failure error code. + public func main(defaultVerb: String, errorHandler: (ClientError) -> ()) -> Never { + main(arguments: CommandLine.arguments, defaultVerb: defaultVerb, errorHandler: errorHandler) + } + + /// Hands off execution to the CommandRegistry, by parsing `arguments` + /// and then running whichever command has been identified in the argument + /// list. + /// + /// If the chosen command executes successfully, the process will exit with + /// a successful exit code. + /// + /// If the chosen command fails, the provided error handler will be invoked, + /// then the process will exit with a failure exit code. + /// + /// If a matching command could not be found but there is any `executable-verb` + /// style subcommand executable in the caller's `$PATH`, the subcommand will + /// be executed. + /// + /// If a matching command could not be found or a usage error occurred, + /// a helpful error message will be written to `stderr`, then the process + /// will exit with a failure error code. + public func main(arguments: [String], defaultVerb: String, errorHandler: (ClientError) -> ()) -> Never { + assert(arguments.count >= 1) + + var arguments = arguments + + // Extract the executable name. + let executableName = arguments.remove(at: 0) + + let verb = arguments.first ?? defaultVerb + if arguments.count > 0 { + // Remove the command name. + arguments.remove(at: 0) + } + + switch run(command: verb, arguments: arguments) { + case .success?: + exit(EXIT_SUCCESS) + + case let .failure(error)?: + switch error { + case let .usageError(description): + fputs(description + "\n", stderr) + + case let .commandError(error): + errorHandler(error) + } + + exit(EXIT_FAILURE) + + case nil: + if let subcommandExecuted = executeSubcommandIfExists(executableName, verb: verb, arguments: arguments) { + exit(subcommandExecuted) + } + + fputs("Unrecognized command: '\(verb)'. See `\(executableName) help`.\n", stderr) + exit(EXIT_FAILURE) + } + } + + /// Finds and executes a subcommand which exists in your $PATH. The executable + /// name must be in the form of `executable-verb`. + /// + /// - Returns: The exit status of found subcommand or nil. + private func executeSubcommandIfExists(_ executableName: String, verb: String, arguments: [String]) -> Int32? { + let subcommand = "\(NSString(string: executableName).lastPathComponent)-\(verb)" + + func launchTask(_ path: String, arguments: [String]) -> Int32 { + let task = Process() + task.launchPath = path + task.arguments = arguments + + task.launch() + task.waitUntilExit() + + return task.terminationStatus + } + + guard launchTask("/usr/bin/which", arguments: [ "-s", subcommand ]) == 0 else { + return nil + } + + return launchTask("/usr/bin/env", arguments: [ subcommand ] + arguments) + } +} + +// MARK: - migration support +@available(*, unavailable, renamed: "CommandProtocol") +public typealias CommandType = CommandProtocol + +extension CommandRegistry { + @available(*, unavailable, renamed: "run(command:arguments:)") + public func runCommand(_ verb: String, arguments: [String]) -> Result<(), CommandantError>? { + return run(command: verb, arguments: arguments) + } +} diff --git a/Seeds/Commandant/Sources/Commandant/Commandant.h b/Seeds/Commandant/Sources/Commandant/Commandant.h new file mode 100644 index 0000000..4507251 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/Commandant.h @@ -0,0 +1,17 @@ +// +// Commandant.h +// Commandant +// +// Created by Justin Spahr-Summers on 2014-11-21. +// Copyright (c) 2014 Carthage. All rights reserved. +// + +#import + +//! Project version number for Commandant. +FOUNDATION_EXPORT double CommandantVersionNumber; + +//! Project version string for Commandant. +FOUNDATION_EXPORT const unsigned char CommandantVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Seeds/Commandant/Sources/Commandant/Errors.swift b/Seeds/Commandant/Sources/Commandant/Errors.swift new file mode 100644 index 0000000..0e3eddf --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/Errors.swift @@ -0,0 +1,147 @@ +// +// Errors.swift +// Commandant +// +// Created by Justin Spahr-Summers on 2014-10-24. +// Copyright (c) 2014 Carthage. All rights reserved. +// + +import Foundation + +/// Possible errors that can originate from Commandant. +/// +/// `ClientError` should be the type of error (if any) that can occur when +/// running commands. +public enum CommandantError: Error { + /// An option was used incorrectly. + case usageError(description: String) + + /// An error occurred while running a command. + case commandError(ClientError) +} + +extension CommandantError: CustomStringConvertible { + public var description: String { + switch self { + case let .usageError(description): + return description + + case let .commandError(error): + return String(describing: error) + } + } +} + +/// Constructs an `InvalidArgument` error that indicates a missing value for +/// the argument by the given name. +internal func missingArgumentError(_ argumentName: String) -> CommandantError { + let description = "Missing argument for \(argumentName)" + return .usageError(description: description) +} + +/// Constructs an error by combining the example of key (and value, if applicable) +/// with the usage description. +internal func informativeUsageError(_ keyValueExample: String, usage: String) -> CommandantError { + let lines = usage.components(separatedBy: .newlines) + + return .usageError(description: lines.reduce(keyValueExample) { previous, value in + return previous + "\n\t" + value + }) +} + +/// Combines the text of the two errors, if they're both `UsageError`s. +/// Otherwise, uses whichever one is not (biased toward the left). +internal func combineUsageErrors(_ lhs: CommandantError, _ rhs: CommandantError) -> CommandantError { + switch (lhs, rhs) { + case let (.usageError(left), .usageError(right)): + let combinedDescription = "\(left)\n\n\(right)" + return .usageError(description: combinedDescription) + + case (.usageError, _): + return rhs + + case (_, .usageError), (_, _): + return lhs + } +} + +/// Constructs an error that indicates unrecognized arguments remains. +internal func unrecognizedArgumentsError(_ options: [String]) -> CommandantError { + return .usageError(description: "Unrecognized arguments: " + options.joined(separator: ", ")) +} + +// MARK: Argument + +/// Constructs an error that describes how to use the argument, with the given +/// example of value usage if applicable. +internal func informativeUsageError(_ valueExample: String, argument: Argument) -> CommandantError { + if argument.defaultValue != nil { + return informativeUsageError("[\(valueExample)]", usage: argument.usage) + } else { + return informativeUsageError(valueExample, usage: argument.usage) + } +} + +/// Constructs an error that describes how to use the argument. +internal func informativeUsageError(_ argument: Argument) -> CommandantError { + var example = "" + + var valueExample = "" + if let defaultValue = argument.defaultValue { + valueExample = "\(defaultValue)" + } + + if valueExample.isEmpty { + example += "(\(T.name))" + } else { + example += valueExample + } + + return informativeUsageError(example, argument: argument) +} + +/// Constructs an error that describes how to use the argument list. +internal func informativeUsageError(_ argument: Argument<[T]>) -> CommandantError { + var example = "" + + var valueExample = "" + if let defaultValue = argument.defaultValue { + valueExample = "\(defaultValue)" + } + + if valueExample.isEmpty { + example += "(\(T.name))" + } else { + example += valueExample + } + + return informativeUsageError(example, argument: argument) +} + +// MARK: Option + +/// Constructs an error that describes how to use the option, with the given +/// example of key (and value, if applicable) usage. +internal func informativeUsageError(_ keyValueExample: String, option: Option) -> CommandantError { + return informativeUsageError("[\(keyValueExample)]", usage: option.usage) +} + +/// Constructs an error that describes how to use the option. +internal func informativeUsageError(_ option: Option) -> CommandantError { + return informativeUsageError("--\(option.key) \(option.defaultValue)", option: option) +} + +/// Constructs an error that describes how to use the option. +internal func informativeUsageError(_ option: Option) -> CommandantError { + return informativeUsageError("--\(option.key) (\(T.name))", option: option) +} + +/// Constructs an error that describes how to use the given boolean option. +internal func informativeUsageError(_ option: Option) -> CommandantError { + let key = option.key + return informativeUsageError((option.defaultValue ? "--no-\(key)" : "--\(key)"), option: option) +} + +// MARK: - migration support +@available(*, unavailable, message: "Use ErrorProtocol instead of ClientErrorType") +public typealias ClientErrorType = Error diff --git a/Seeds/Commandant/Sources/Commandant/HelpCommand.swift b/Seeds/Commandant/Sources/Commandant/HelpCommand.swift new file mode 100644 index 0000000..90ef6a7 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/HelpCommand.swift @@ -0,0 +1,75 @@ +// +// HelpCommand.swift +// Commandant +// +// Created by Justin Spahr-Summers on 2014-10-10. +// Copyright (c) 2014 Carthage. All rights reserved. +// + +import Foundation + +/// A basic implementation of a `help` command, using information available in a +/// `CommandRegistry`. +/// +/// If you want to use this command, initialize it with the registry, then add +/// it to that same registry: +/// +/// let commands: CommandRegistry = … +/// let helpCommand = HelpCommand(registry: commands) +/// commands.register(helpCommand) +public struct HelpCommand: CommandProtocol { + public typealias Options = HelpOptions + + public let verb = "help" + public let function = "Display general or command-specific help" + + private let registry: CommandRegistry + + /// Initializes the command to provide help from the given registry of + /// commands. + public init(registry: CommandRegistry) { + self.registry = registry + } + + public func run(_ options: Options) -> Result<(), ClientError> { + if let verb = options.verb { + if let command = self.registry[verb] { + print(command.function) + if let usageError = command.usage() { + print("\n\(usageError)") + } + return .success(()) + } else { + fputs("Unrecognized command: '\(verb)'\n", stderr) + } + } + + print("Available commands:\n") + + let maxVerbLength = self.registry.commands.map { $0.verb.characters.count }.max() ?? 0 + + for command in self.registry.commands { + let padding = repeatElement(Character(" "), count: maxVerbLength - command.verb.characters.count) + print(" \(command.verb)\(String(padding)) \(command.function)") + } + + return .success(()) + } +} + +public struct HelpOptions: OptionsProtocol { + fileprivate let verb: String? + + private init(verb: String?) { + self.verb = verb + } + + private static func create(_ verb: String) -> HelpOptions { + return self.init(verb: (verb == "" ? nil : verb)) + } + + public static func evaluate(_ m: CommandMode) -> Result> { + return create + <*> m <| Argument(defaultValue: "", usage: "the command to display help for") + } +} diff --git a/Seeds/Commandant/Sources/Commandant/Info.plist b/Seeds/Commandant/Sources/Commandant/Info.plist new file mode 100644 index 0000000..43f91b3 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.11.1 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2014 Carthage. All rights reserved. + NSPrincipalClass + + + diff --git a/Seeds/Commandant/Sources/Commandant/LinuxSupport.swift b/Seeds/Commandant/Sources/Commandant/LinuxSupport.swift new file mode 100644 index 0000000..847653c --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/LinuxSupport.swift @@ -0,0 +1,14 @@ +// +// LinuxSupport.swift +// Commandant +// +// Created by Norio Nomura on 3/26/16. +// Copyright © 2016 Carthage. All rights reserved. +// + +import Foundation + +// swift-corelibs-foundation is still written in Swift 2 API. +#if os(Linux) + typealias Process = Task +#endif diff --git a/Seeds/Commandant/Sources/Commandant/Option.swift b/Seeds/Commandant/Sources/Commandant/Option.swift new file mode 100644 index 0000000..ab7b618 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/Option.swift @@ -0,0 +1,214 @@ +// +// Option.swift +// Commandant +// +// Created by Justin Spahr-Summers on 2014-11-21. +// Copyright (c) 2014 Carthage. All rights reserved. +// + +import Foundation + +/// Represents a record of options for a command, which can be parsed from +/// a list of command-line arguments. +/// +/// This is most helpful when used in conjunction with the `Option` and `Switch` +/// types, and `<*>` and `<|` combinators. +/// +/// Example: +/// +/// struct LogOptions: OptionsProtocol { +/// let verbosity: Int +/// let outputFilename: String +/// let logName: String +/// +/// static func create(verbosity: Int)(outputFilename: String)(logName: String) -> LogOptions { +/// return LogOptions(verbosity: verbosity, outputFilename: outputFilename, logName: logName) +/// } +/// +/// static func evaluate(m: CommandMode) -> Result> { +/// return create +/// <*> m <| Option(key: "verbose", defaultValue: 0, usage: "the verbosity level with which to read the logs") +/// <*> m <| Option(key: "outputFilename", defaultValue: "", usage: "a file to print output to, instead of stdout") +/// <*> m <| Switch(flag: "d", key: "delete", defaultValue: false, usage: "delete the logs when finished") +/// <*> m <| Option(usage: "the log to read") +/// } +/// } +public protocol OptionsProtocol { + associatedtype ClientError: Error + + /// Evaluates this set of options in the given mode. + /// + /// Returns the parsed options or a `UsageError`. + static func evaluate(_ m: CommandMode) -> Result> +} + +/// An `OptionsProtocol` that has no options. +public struct NoOptions: OptionsProtocol { + public init() {} + + public static func evaluate(_ m: CommandMode) -> Result> { + return .success(NoOptions()) + } +} + +/// Describes an option that can be provided on the command line. +public struct Option { + /// The key that controls this option. For example, a key of `verbose` would + /// be used for a `--verbose` option. + public let key: String + + /// The default value for this option. This is the value that will be used + /// if the option is never explicitly specified on the command line. + public let defaultValue: T + + /// A human-readable string describing the purpose of this option. This will + /// be shown in help messages. + /// + /// For boolean operations, this should describe the effect of _not_ using + /// the default value (i.e., what will happen if you disable/enable the flag + /// differently from the default). + public let usage: String + + public init(key: String, defaultValue: T, usage: String) { + self.key = key + self.defaultValue = defaultValue + self.usage = usage + } +} + +extension Option: CustomStringConvertible { + public var description: String { + return "--\(key)" + } +} + +// Inspired by the Argo library: +// https://github.com/thoughtbot/Argo +/* + Copyright (c) 2014 thoughtbot, inc. + + MIT License + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +infix operator <*> : LogicalDisjunctionPrecedence + +infix operator <| : MultiplicationPrecedence + +/// Applies `f` to the value in the given result. +/// +/// In the context of command-line option parsing, this is used to chain +/// together the parsing of multiple arguments. See OptionsProtocol for an example. +public func <*> (f: (T) -> U, value: Result>) -> Result> { + return value.map(f) +} + +/// Applies the function in `f` to the value in the given result. +/// +/// In the context of command-line option parsing, this is used to chain +/// together the parsing of multiple arguments. See OptionsProtocol for an example. +public func <*> (f: Result<((T) -> U), CommandantError>, value: Result>) -> Result> { + switch (f, value) { + case let (.failure(left), .failure(right)): + return .failure(combineUsageErrors(left, right)) + + case let (.failure(left), .success): + return .failure(left) + + case let (.success, .failure(right)): + return .failure(right) + + case let (.success(f), .success(value)): + let newValue = f(value) + return .success(newValue) + } +} + +/// Evaluates the given option in the given mode. +/// +/// If parsing command line arguments, and no value was specified on the command +/// line, the option's `defaultValue` is used. +public func <| (mode: CommandMode, option: Option) -> Result> { + let wrapped = Option(key: option.key, defaultValue: option.defaultValue, usage: option.usage) + // Since we are passing a non-nil default value, we can safely unwrap the + // result. + return (mode <| wrapped).map { $0! } +} + +/// Evaluates the given option in the given mode. +/// +/// If parsing command line arguments, and no value was specified on the command +/// line, `nil` is used. +public func <| (mode: CommandMode, option: Option) -> Result> { + let key = option.key + switch mode { + case let .arguments(arguments): + var stringValue: String? + switch arguments.consumeValue(forKey: key) { + case let .success(value): + stringValue = value + + case let .failure(error): + switch error { + case let .usageError(description): + return .failure(.usageError(description: description)) + + case .commandError: + fatalError("CommandError should be impossible when parameterized over NoError") + } + } + + if let stringValue = stringValue { + if let value = T.from(string: stringValue) { + return .success(value) + } + + let description = "Invalid value for '--\(key)': \(stringValue)" + return .failure(.usageError(description: description)) + } else { + return .success(option.defaultValue) + } + + case .usage: + return .failure(informativeUsageError(option)) + } +} + +/// Evaluates the given boolean option in the given mode. +/// +/// If parsing command line arguments, and no value was specified on the command +/// line, the option's `defaultValue` is used. +public func <| (mode: CommandMode, option: Option) -> Result> { + switch mode { + case let .arguments(arguments): + if let value = arguments.consumeBoolean(forKey: option.key) { + return .success(value) + } else { + return .success(option.defaultValue) + } + + case .usage: + return .failure(informativeUsageError(option)) + } +} + +// MARK: - migration support +@available(*, unavailable, renamed: "OptionsProtocol") +public typealias OptionsType = OptionsProtocol diff --git a/Seeds/Commandant/Sources/Commandant/Switch.swift b/Seeds/Commandant/Sources/Commandant/Switch.swift new file mode 100644 index 0000000..b41e4e0 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/Switch.swift @@ -0,0 +1,63 @@ +// +// Switch.swift +// Commandant +// +// Created by Neil Pankey on 3/31/15. +// Copyright (c) 2015 Carthage. All rights reserved. +// + + +/// Describes a parameterless command line flag that defaults to false and can only +/// be switched on. Canonical examples include `--force` and `--recurse`. +/// +/// For a boolean toggle that can be enabled and disabled use `Option`. +public struct Switch { + /// The key that enables this switch. For example, a key of `verbose` would be + /// used for a `--verbose` option. + public let key: String + + /// Optional single letter flag that enables this switch. For example, `-v` would + /// be used as a shorthand for `--verbose`. + /// + /// Multiple flags can be grouped together as a single argument and will split + /// when parsing (e.g. `rm -rf` treats 'r' and 'f' as inidividual flags). + public let flag: Character? + + /// A human-readable string describing the purpose of this option. This will + /// be shown in help messages. + public let usage: String + + public init(flag: Character? = nil, key: String, usage: String) { + self.flag = flag + self.key = key + self.usage = usage + } +} + +extension Switch: CustomStringConvertible { + public var description: String { + var options = "--\(key)" + if let flag = self.flag { + options += "|-\(flag)" + } + return options + } +} + +/// Evaluates the given boolean switch in the given mode. +/// +/// If parsing command line arguments, and no value was specified on the command +/// line, the option's `defaultValue` is used. +public func <| (mode: CommandMode, option: Switch) -> Result> { + switch mode { + case let .arguments(arguments): + var enabled = arguments.consume(key: option.key) + if let flag = option.flag { + enabled = arguments.consumeBoolean(flag: flag) + } + return .success(enabled) + + case .usage: + return .failure(informativeUsageError(option.description, usage: option.usage)) + } +} diff --git a/Seeds/Result/Result/Result.h b/Seeds/Result/Result/Result.h new file mode 100644 index 0000000..4742701 --- /dev/null +++ b/Seeds/Result/Result/Result.h @@ -0,0 +1,8 @@ +// Copyright (c) 2015 Rob Rix. All rights reserved. + +/// Project version number for Result. +extern double ResultVersionNumber; + +/// Project version string for Result. +extern const unsigned char ResultVersionString[]; + diff --git a/Seeds/Result/Result/Result.swift b/Seeds/Result/Result/Result.swift new file mode 100644 index 0000000..e8c7adb --- /dev/null +++ b/Seeds/Result/Result/Result.swift @@ -0,0 +1,192 @@ +// Copyright (c) 2015 Rob Rix. All rights reserved. + +/// An enum representing either a failure with an explanatory error, or a success with a result value. +public enum Result: ResultProtocol, CustomStringConvertible, CustomDebugStringConvertible { + case success(T) + case failure(Error) + + // MARK: Constructors + + /// Constructs a success wrapping a `value`. + public init(value: T) { + self = .success(value) + } + + /// Constructs a failure wrapping an `error`. + public init(error: Error) { + self = .failure(error) + } + + /// Constructs a result from an Optional, failing with `Error` if `nil`. + public init(_ value: T?, failWith: @autoclosure () -> Error) { + self = value.map(Result.success) ?? .failure(failWith()) + } + + /// Constructs a result from a function that uses `throw`, failing with `Error` if throws. + public init(_ f: @autoclosure () throws -> T) { + self.init(attempt: f) + } + + /// Constructs a result from a function that uses `throw`, failing with `Error` if throws. + public init(attempt f: () throws -> T) { + do { + self = .success(try f()) + } catch { + self = .failure(error as! Error) + } + } + + // MARK: Deconstruction + + /// Returns the value from `Success` Results or `throw`s the error. + public func dematerialize() throws -> T { + switch self { + case let .success(value): + return value + case let .failure(error): + throw error + } + } + + /// Case analysis for Result. + /// + /// Returns the value produced by applying `ifFailure` to `Failure` Results, or `ifSuccess` to `Success` Results. + public func analysis(ifSuccess: (T) -> Result, ifFailure: (Error) -> Result) -> Result { + switch self { + case let .success(value): + return ifSuccess(value) + case let .failure(value): + return ifFailure(value) + } + } + + // MARK: Errors + + /// The domain for errors constructed by Result. + public static var errorDomain: String { return "com.antitypical.Result" } + + /// The userInfo key for source functions in errors constructed by Result. + public static var functionKey: String { return "\(errorDomain).function" } + + /// The userInfo key for source file paths in errors constructed by Result. + public static var fileKey: String { return "\(errorDomain).file" } + + /// The userInfo key for source file line numbers in errors constructed by Result. + public static var lineKey: String { return "\(errorDomain).line" } + + /// Constructs an error. + public static func error(_ message: String? = nil, function: String = #function, file: String = #file, line: Int = #line) -> NSError { + var userInfo: [String: Any] = [ + functionKey: function, + fileKey: file, + lineKey: line, + ] + + if let message = message { + userInfo[NSLocalizedDescriptionKey] = message + } + + return NSError(domain: errorDomain, code: 0, userInfo: userInfo) + } + + + // MARK: CustomStringConvertible + + public var description: String { + return analysis( + ifSuccess: { ".success(\($0))" }, + ifFailure: { ".failure(\($0))" }) + } + + + // MARK: CustomDebugStringConvertible + + public var debugDescription: String { + return description + } +} + +// MARK: - Derive result from failable closure + +public func materialize(_ f: () throws -> T) -> Result { + return materialize(try f()) +} + +public func materialize(_ f: @autoclosure () throws -> T) -> Result { + do { + return .success(try f()) + } catch let error as NSError { + return .failure(error) + } +} + +// MARK: - Cocoa API conveniences + +#if !os(Linux) + +/// Constructs a Result with the result of calling `try` with an error pointer. +/// +/// This is convenient for wrapping Cocoa API which returns an object or `nil` + an error, by reference. e.g.: +/// +/// Result.try { NSData(contentsOfURL: URL, options: .DataReadingMapped, error: $0) } +public func `try`(_ function: String = #function, file: String = #file, line: Int = #line, `try`: (NSErrorPointer) -> T?) -> Result { + var error: NSError? + return `try`(&error).map(Result.success) ?? .failure(error ?? Result.error(function: function, file: file, line: line)) +} + +/// Constructs a Result with the result of calling `try` with an error pointer. +/// +/// This is convenient for wrapping Cocoa API which returns a `Bool` + an error, by reference. e.g.: +/// +/// Result.try { NSFileManager.defaultManager().removeItemAtURL(URL, error: $0) } +public func `try`(_ function: String = #function, file: String = #file, line: Int = #line, `try`: (NSErrorPointer) -> Bool) -> Result<(), NSError> { + var error: NSError? + return `try`(&error) ? + .success(()) + : .failure(error ?? Result<(), NSError>.error(function: function, file: file, line: line)) +} + +#endif + +// MARK: - ErrorProtocolConvertible conformance + +extension NSError: ErrorProtocolConvertible { + public static func error(from error: Swift.Error) -> Self { + func cast(_ error: Swift.Error) -> T { + return error as! T + } + + return cast(error) + } +} + +// MARK: - + +/// An “error” that is impossible to construct. +/// +/// This can be used to describe `Result`s where failures will never +/// be generated. For example, `Result` describes a result that +/// contains an `Int`eger and is guaranteed never to be a `Failure`. +public enum NoError: Swift.Error { } + +// MARK: - migration support +extension Result { + @available(*, unavailable, renamed: "success") + public static func Success(_: T) -> Result { + fatalError() + } + + @available(*, unavailable, renamed: "failure") + public static func Failure(_: Error) -> Result { + fatalError() + } +} + +extension NSError { + @available(*, unavailable, renamed: "error(from:)") + public static func errorFromErrorType(_ error: Swift.Error) -> Self { + fatalError() + } +} + +import Foundation diff --git a/Seeds/Result/Result/ResultProtocol.swift b/Seeds/Result/Result/ResultProtocol.swift new file mode 100644 index 0000000..2bfdffb --- /dev/null +++ b/Seeds/Result/Result/ResultProtocol.swift @@ -0,0 +1,182 @@ +// Copyright (c) 2015 Rob Rix. All rights reserved. + +/// A type that can represent either failure with an error or success with a result value. +public protocol ResultProtocol { + associatedtype Value + associatedtype Error: Swift.Error + + /// Constructs a successful result wrapping a `value`. + init(value: Value) + + /// Constructs a failed result wrapping an `error`. + init(error: Error) + + /// Case analysis for ResultProtocol. + /// + /// Returns the value produced by appliying `ifFailure` to the error if self represents a failure, or `ifSuccess` to the result value if self represents a success. + func analysis(ifSuccess: (Value) -> U, ifFailure: (Error) -> U) -> U + + /// Returns the value if self represents a success, `nil` otherwise. + /// + /// A default implementation is provided by a protocol extension. Conforming types may specialize it. + var value: Value? { get } + + /// Returns the error if self represents a failure, `nil` otherwise. + /// + /// A default implementation is provided by a protocol extension. Conforming types may specialize it. + var error: Error? { get } +} + +public extension ResultProtocol { + + /// Returns the value if self represents a success, `nil` otherwise. + public var value: Value? { + return analysis(ifSuccess: { $0 }, ifFailure: { _ in nil }) + } + + /// Returns the error if self represents a failure, `nil` otherwise. + public var error: Error? { + return analysis(ifSuccess: { _ in nil }, ifFailure: { $0 }) + } + + /// Returns a new Result by mapping `Success`es’ values using `transform`, or re-wrapping `Failure`s’ errors. + public func map(_ transform: (Value) -> U) -> Result { + return flatMap { .success(transform($0)) } + } + + /// Returns the result of applying `transform` to `Success`es’ values, or re-wrapping `Failure`’s errors. + public func flatMap(_ transform: (Value) -> Result) -> Result { + return analysis( + ifSuccess: transform, + ifFailure: Result.failure) + } + + /// Returns a new Result by mapping `Failure`'s values using `transform`, or re-wrapping `Success`es’ values. + public func mapError(_ transform: (Error) -> Error2) -> Result { + return flatMapError { .failure(transform($0)) } + } + + /// Returns the result of applying `transform` to `Failure`’s errors, or re-wrapping `Success`es’ values. + public func flatMapError(_ transform: (Error) -> Result) -> Result { + return analysis( + ifSuccess: Result.success, + ifFailure: transform) + } +} + +public extension ResultProtocol { + + // MARK: Higher-order functions + + /// Returns `self.value` if this result is a .Success, or the given value otherwise. Equivalent with `??` + public func recover(_ value: @autoclosure () -> Value) -> Value { + return self.value ?? value() + } + + /// Returns this result if it is a .Success, or the given result otherwise. Equivalent with `??` + public func recover(with result: @autoclosure () -> Self) -> Self { + return analysis( + ifSuccess: { _ in self }, + ifFailure: { _ in result() }) + } +} + +/// Protocol used to constrain `tryMap` to `Result`s with compatible `Error`s. +public protocol ErrorProtocolConvertible: Swift.Error { + static func error(from error: Swift.Error) -> Self +} + +public extension ResultProtocol where Error: ErrorProtocolConvertible { + + /// Returns the result of applying `transform` to `Success`es’ values, or wrapping thrown errors. + public func tryMap(_ transform: (Value) throws -> U) -> Result { + return flatMap { value in + do { + return .success(try transform(value)) + } + catch { + let convertedError = Error.error(from: error) + // Revisit this in a future version of Swift. https://twitter.com/jckarter/status/672931114944696321 + return .failure(convertedError) + } + } + } +} + +// MARK: - Operators + +infix operator &&& : LogicalConjunctionPrecedence + +/// Returns a Result with a tuple of `left` and `right` values if both are `Success`es, or re-wrapping the error of the earlier `Failure`. +public func &&& (left: L, right: @autoclosure () -> R) -> Result<(L.Value, R.Value), L.Error> + where L.Error == R.Error +{ + return left.flatMap { left in right().map { right in (left, right) } } +} + +precedencegroup ChainingPrecedence { + associativity: left + higherThan: TernaryPrecedence +} + +infix operator >>- : ChainingPrecedence + +/// Returns the result of applying `transform` to `Success`es’ values, or re-wrapping `Failure`’s errors. +/// +/// This is a synonym for `flatMap`. +public func >>- (result: T, transform: (T.Value) -> Result) -> Result { + return result.flatMap(transform) +} + +/// Returns `true` if `left` and `right` are both `Success`es and their values are equal, or if `left` and `right` are both `Failure`s and their errors are equal. +public func == (left: T, right: T) -> Bool + where T.Value: Equatable, T.Error: Equatable +{ + if let left = left.value, let right = right.value { + return left == right + } else if let left = left.error, let right = right.error { + return left == right + } + return false +} + +/// Returns `true` if `left` and `right` represent different cases, or if they represent the same case but different values. +public func != (left: T, right: T) -> Bool + where T.Value: Equatable, T.Error: Equatable +{ + return !(left == right) +} + +/// Returns the value of `left` if it is a `Success`, or `right` otherwise. Short-circuits. +public func ?? (left: T, right: @autoclosure () -> T.Value) -> T.Value { + return left.recover(right()) +} + +/// Returns `left` if it is a `Success`es, or `right` otherwise. Short-circuits. +public func ?? (left: T, right: @autoclosure () -> T) -> T { + return left.recover(with: right()) +} + +// MARK: - migration support +@available(*, unavailable, renamed: "ResultProtocol") +public typealias ResultType = ResultProtocol + +@available(*, unavailable, renamed: "Error") +public typealias ResultErrorType = Swift.Error + +@available(*, unavailable, renamed: "ErrorProtocolConvertible") +public typealias ErrorTypeConvertible = ErrorProtocolConvertible + +extension ResultProtocol { + @available(*, unavailable, renamed: "recover(with:)") + public func recoverWith(_ result: @autoclosure () -> Self) -> Self { + fatalError() + } +} + +extension ErrorProtocolConvertible { + @available(*, unavailable, renamed: "error(from:)") + public static func errorFromErrorType(_ error: Swift.Error) -> Self { + fatalError() + } +}