Vapor's Console package (which will be renamed to ConsoleKit) is an API for building command-line applications with Swift. This pitch proposes changing the current stringly typed API for getting console input to a type-safe API that would:
- Only allow you to attempt to get input that you defined for an app command.
- Automatically convert the string input the desired input type.
Current API
In Console 3, when you define a command, your arguments and options look like this:
final class SomeCommand: Command {
public var arguments: [CommandArgument] = [
CommandArgument.argument(name: "string", help: ["The string to log to stdout"]),
CommandArgument.argument(name: "count", help: ["How many times to log the string"])
]
public var options: [CommandOption] = [
CommandOption.flag(name: "verbose", short: "v", help: ["Logs each string on its own line"])
]
}
To get the input values, you pass in the name of the argument/option into the console.argument
or .option
method as a string. Then you have to convert from a string to the type you actually want if it is not a string:
let countStr = try context.console.argument("count")
let count = Int(count)
This stringly typed API has several undesirable attributes, including it being susceptible to typos and requiring extra operations to get the type you want to work with.
Proposed API
The proposed solution to this is to add associatedtype requirements to the Command
protocol which will be a cased type (enum
or struct
with static properties) that represents the valid arguments/options. You will then be able to pass a case into the console.argument
or .option
method.
Enums
Enums are the most obvious way to go with defining the Arguments
and Options
types. Here's why we would and wouldn't want to use enums:
Pros:
- Significantly reduced memory footprint.
Cons:
- Declaring the
name
andhelp
values is more verbose. - Cannot define generic types to decode the argument or option value to.
A command's signature defined with an enum would look like this:
enum Arguments: String, CommandArgument {
case string, count
var help: String? {
switch self {
case .string: return "The string to print to srdout"
case .count: "The number of times to output the string"
}
}
}
enum Options: String, CommandOption {
case verbose
var help: String? {
switch self {
case .verbose: return "List each output on its own line"
}
}
var shortFlag: Character? {
switch self {
case .verbose: return "v"
}
}
}
Cased Structs
If we used a struct, it wouldn't actually have static cases because using enums would actually be easier. Instead we would use key-paths for the properties.
Pros:
- Defining the
name
andhelp
properties is much more concise. - You can define generic type to automatically convert argument/option values to.
Cons:
- Takes more space in memory.
A command's signature defined using structs would look like this:
struct Arguments: CommandArguments {
var string = Argument<String>(name: "string", help: "The string to print to srdout")
var count = Argument<Int>(name: "count", help: "The number of times to output the string")
}
struct Options: CommandOptions {
var verbose = Option<Bool>(name: "verbose", short: "v", help: "List each output on its own line")
}
A couple notes about how the structs would be handled.
- The
Options
andArguments
struct would require an initializer that takes no arguments. These instances would then be stored in the command context. - The
Option
andArgument
structs would take in the arguments as auto-closures and return the values from computed properties if they are needed.
Raw Input
Sometimes you need to just take in raw input. An UnvalidatedCommand
protocol (or some other name) which will allow you to do this.
Call For Discussion
At this point we still need to decide which way to take this, so post your opinions and ideas!
Implementation:
You can see the current implementation and watch development on this PR: https://github.com/vapor/console/pull/92