Commander is a Swift framework for decoding command-line arguments by integrating with Swift standard library protocols Decodable & Decoder. Commander can help you to write structured cli program by declaring the structure of command
and options
of that command without writing any codes to parse the cli arguments. With Commander, you just need to focus on writing options
structure of commands, the rest works will be handled by Commander automatically.ios
struct
or class
.Decodable
protocol.commander
or command
.swift build
.With Commander, a command and its associated options could be defined as follows:shell
import Commander
public struct SampleCommand: CommandRepresentable {
public struct Options: OptionsRepresentable {
public typealias ArgumentsResolver = AnyArgumentsResolver<String>
public enum CodingKeys: String, CodingKeysRepresentable {
case verbose = "verbose"
case stringValue = "string-value"
}
public static let keys: [Options.CodingKeys : Character] = [
.verbose: "v",
.stringValue: "s"
]
public static let descriptions: [Options.CodingKeys : OptionDescription] = [
.verbose: .usage("Prints the logs of the command"),
.stringValue: .usage("Pass a value of String to the command")
]
public var verbose: Bool = false
public var stringValue: String = ""
}
public static let symbol: String = "sample"
public static let usage: String = "Show sample usage of commander"
public static func main(_ options: Options) throws {
print(options)
print("arguments: \(options.arguments)")
print("\n\n\(Options.CodingKeys.stringValue.stringValue)")
}
}
複製代碼
Then, configuring the available commands would like this:swift
import Commander
BuildIn.Commander.commands = [
SampleCommand.self,
NoArgsCommand.self
]
BuiltIn.Commander.usage = "The sample usage command of 'Commander'"
BuiltIn.Commander().dispatch() // Call this to dispatch and run the command
複製代碼
After which, arguments can be resolved by declaration of ArgumentsResolver
:api
public typealias ArgumentsResolver = AnyArgumentsResolver<T> // T must be Decodable
複製代碼
And you can fetch the arguments by:bash
public static func main(_ options: Options) throws {
print("arguments: \(options.arguments)") // 'arguments' is being declared in OptionsRepresentable
}
複製代碼
At last, run from shell:app
commander-sample sample --verbose --string-value String arg1 arg2
#
# Options(verbose: true, stringValue: "String")
# arguments: ["arg1", "arg2"]
#
#
# string-value
複製代碼
It's easy and fun!!!ide
Requirements - Test Coverage Graph - Installation svg
// swift-tools-version:4.2
dependencies: [
.package(url: "https://github.com/devedbox/Commander.git", "0.5.6..<100.0.0")
]
複製代碼
commander command --key value --key1=value1
commander command --bool
commander command -k value -K=value1
commander command -z=value # {"z": "value"}
commander command -z # {"z": true}
commander command -zop # {"z": true, "o": true, "p": true}
複製代碼
commander command --array val1,val2,val3
commander command -a val1,val2,val3
commander command --dict key1=val1,key2=val2,key3=val3
commander command -d key1=val1,key2=val2,key3=val3
commander command --array val1 --array val2 --array val3
commander command -a val1 -a val2 -a val3
commander command --dict key1=val1 --dict key2=val2 --dict key3=val3
commander command -d key1=val1 -d key2=val2 -d key3=val3
複製代碼
In Commander,The position of arguments is not settled, they can be arywhere but the arguments must be continuous:
commander command args... --options # before options
commander command --options args... # after options
commander command --options args... --options # between options
commander command arg0... --options arg1... --options # Error
複製代碼
Use --
to mark the ends of options and begins of arguments, but, this is normally optional in Commander:
commander command --options -- args...
As we all know, all the arguments from CommandLine.arguments
is String
type, in Commander, the available value types are:
commander command --verbose
commander command --int 100
commander command --string "this is a string value"
commander command --array val1,val2,val3
commander command --dict key1=val1,key2=val2,key3=val3
Array object is delimited by character ,
and Dict object is delimited by character =
and ,
.
Commander supports a main commander alongwith the commands of that commander, and each command has its own subcommands and options.
Using a Commander is simple, you just need to declare the commands
, usage
of the commander, and then call Commander().dispatch()
, the Commander will automatically decode the command line arguments and dispatch the decoded options to the specific command given by command line.
Just as simple as following:
import Commander
BuiltIn.Commander.commands = [
SampleCommand.self,
NoArgsCommand.self
]
BuiltIn.Commander.usage = "The sample usage command of 'Commander'"
BuiltIn.Commander().dispatch()
複製代碼
In Commander, a command is a type(class
or struct
) that conforms to protocol CommandRepresentable
. The protocol CommandRepresentable declares the infos of the conforming commands:
Options
: The associated type of command's options.symbol
: The symbol of the command used by command line shell.usage
: The usage help message for that command.children
: The subcommands of that command.public struct Hello: CommandRepresentable {
public struct Options: OptionsRepresentable {
public enum CodingKeys: String, CodingKeysRepresentable {
case verbose
}
public static let descriptions: [SampleCommand.Options.CodingKeys : OptionDescription] = [
.verbose: .usage("Prints the logs of the command"),
]
public var verbose: Bool = false
}
public static let symbol: String = "sample"
public static let usage: String = "Show sample usage of commander"
public static func main(_ options: Options) throws {
if options.verbose {
print(options.argiments.first ?? "")
}
}
}
複製代碼
Once a command has been created, it can be dispathed against a list of arguments, usually taken from CommandLine.arguments with dropping of the symbol of command itself.
let arguments = ["sample", "--verbose", "Hello world"]
Command.dispatch(with: arguments.dropFirst())
// Hello world
複製代碼
As a real dispatching of command, you don't need to dispatch the command manually, the dispatching will be handled by Commander automatically.
Adding subcommands in Commander is by declaring the children
of type [CommandDescribable.Type]
:
public struct Hello: CommandRepresentable {
...
public static let children: [CommandDescribable.Type] = [
Subcommand1.self,
Subcommand2.self
]
...
}
複製代碼
The Options
is the same as command, is a type(class
or struct
) that conforms to protocol OptionsRepresentable
which inherited from Decodable
and can be treated as a simple data model, will be decoed by the built in code type OptionsDecoder
in Commander.
As mentioned earlier in Creating a Command, declaring an options type is extremely easy, just another data model represents the raw string in command line arguments:
public struct Options: OptionsRepresentable {
public enum CodingKeys: String, CodingKeysRepresentable {
case verbose
}
public static let descriptions: [SampleCommand.Options.CodingKeys : OptionDescription] = [
.verbose: .usage("Prints the logs of the command"),
]
public var verbose: Bool = false
}
複製代碼
As declared as public var verbose: Bool
, we can use symbol in command line with --verbose
accordingly, but how to use another different symbol in command line to wrap verbose
such as --is-verbose
? In Commander, we can just do as this:
public enum CodingKeys: String, CodingKeysRepresentable {
case verbose = "is-verbose"
}
複製代碼
Sometimes in develping command line tools, using a pattern like -v
is necessary and helpful. In Commander, providing a short key for option is easy, we just need to declare a key-value pairs of type [CodingKeys: Character]
in Options.keys
:
public struct Options: OptionsRepresentable {
...
public static let keys: [CodingKeys: Character] = [
.verbose: "v"
]
...
}
複製代碼
When we difine a flag option in our command, provide a default value for flag is required because if we miss typing the flag in command line, the value of that flag means false
. Providing default value in Commander is by add declaration in Options.descritions
as this:
public struct Options: OptionsRepresentable {
...
public static let descriptions: [SampleCommand.Options.CodingKeys : OptionDescription] = [
.verbose: .default(value: false, usage:"Prints the logs of the command")
]
...
}
複製代碼
In Commander, help menu is generated by CommandDescriber
describing types conforming CommandDescribable
automatically, including commander itself and all declared commands.
To provide help menu usages, in commands:
public struct Hello: CommandRepresentable {
...
public static let symbol: String = "sample"
public static let usage: String = "Show sample usage of commander"
...
}
複製代碼
In options:
public struct Options: OptionsRepresentable {
...
public static let descriptions: [SampleCommand.Options.CodingKeys : OptionDescription] = [
.verbose: .default(value: false, usage:"Prints the logs of the command")
]
...
}
複製代碼
Normally, the help usage message and default can both be provided by type OptionsDescriotion
.
After declaration of usages, run help
in terminal:
commander-sample --help # or, commander-sample help
# Usage:
#
# $ commander-sample [COMMAND] [OPTIONS]
#
# The sample usage command of 'Commander'
#
# Commands:
#
# help Prints the help message of the command. Usage: [help [COMMANDS]]
# sample Show sample usage of commander
# set-args Set arguments of the command with given arguments
#
# Options:
#
# -h, --help Prints the help message of the command. Usage: [[--help|-h][COMMAND --help][COMMAND -h]]
複製代碼
For specific commands, run as this:
commander-sample help sample # or, commander-sample sample --help
# Usage of 'sample':
#
# $ commander-sample sample [SUBCOMMAND] [OPTIONS] [ARGUMENTS]
#
# Show sample usage of commander
#
# Subcommands:
#
# set-args Set arguments of the command with given arguments
#
# Options:
#
# -s, --string-value Pass a value of String to the command
# -h, --help Prints the help message of the command. Usage: [[--help|-h][COMMAND --help][COMMAND -h]]
# -v, --verbose Prints the logs of the command
#
# Arguments:
#
# [String] commander-sample sample [options] arg1 arg2 ...
複製代碼
In Commander, an option can take multiple arguments from command line arguments as the arguments of that option, and can be accessed by calling options.arguments
. The arguments decoding can not be resolvable by default, if you want to resolve the decoding of arguments, you must declare the ArgumentsResolver
of the options:
public struct Options: OptionsRepresentable {
...
public typealias ArgumentsResolver = AnyArgumentsResolver<String>
...
}
複製代碼
The type AnyArgumentsResolver<T>
is generic type where the type T
representing the type of arguments' element. With the declaration above, we can do this is command line:
commander hello --verbose -- "Hello world" "Will be dropped"
# "Hello world" "Will be dropped" are both the arguments of Hello.Options
複製代碼
Commander provided the api to write auto-completion in bash/zsh, the requirement is declared in protocol ShellCompletable
. The CommandDescribable
and OptionsDescribable
is inherited from ShellCompletable
by default.
To implemente auto-completion, you just need to write:
import Commander.Utility
// Options:
public static func completions(for commandLine: Utility.CommandLine) -> [String] {
switch key {
case "--string-value":
return [
"a", "b", "c"
]
default:
return [ ]
}
}
複製代碼
In terminal, type this:
commander sample --string-value <Tab>
# a b c
複製代碼
Commander can generate auto-completion scripts for you, you can run the built-in command complete generate
to generate the scripts according to the shell type. Currently available shells are:
commander complete generate --shell=bash > ./bash_completion
source ./bash_completion
~/.profile
commander complete generate --shell=zsh > ~/zsh_completions/_commander
~/.zshrc
:fpath=(~/zsh_completions $fpath)
autoload -U +X compinit && compinit
autoload -U +X bashcompinit && bashcompinit
複製代碼
CommandDescribable
already provided the default implementation of completion, by default, CommandDescribable provides the subcommands alongwith options as the completions for shell, you can override the default implementation to provide your custom completions to Commander.
OptionsDescribable
returns an empty completions by default, OptionsDescribable will be called during the calling of CommandDescribable automatically, You must override the implementation of OptionsDescribable to provide your completions or an empty completions will be used.
This is an example to provide git branchs
completions to shell:
import Commander.Utility
public static func completions(for commandLine: Utility.CommandLine) -> [String] {
let current = commandLine.arguments.last
let previous = commandLine.arguments.dropLast().last
switch current {
default:
let outputs = ShellIn("git branch -r").execute().output.flatMap {
String(data: $0, encoding: .utf8)
} ?? ""
return outputs.split(whereSeparator: {
" *->\n".contains($0)
}).map {
if $0.hasPrefix("origin/") {
return String(String($0)["origin/".endIndex...])
} else {
return String($0)
}
}
}
}
複製代碼
Commander is released under the MIT license.