Giter Site home page Giter Site logo

swiftcli's Introduction

SwiftCLI

Build Status

A powerful framework for developing CLIs, from the simplest to the most complex, in Swift.

import SwiftCLI

class GreetCommand: Command {
    let name = "greet"
    
    @Param var person: String

    func execute() throws {
        stdout <<< "Hello \(person)!"
    }
}

let greeter = CLI(name: "greeter")
greeter.commands = [GreetCommand()]
greeter.go()
~ > greeter greet world
Hello world!

With SwiftCLI, you automatically get:

  • Command routing
  • Option parsing
  • Help messages
  • Usage statements
  • Error messages when commands are used incorrectly
  • Zsh completions

Table of Contents

Installation

> ice add jakeheis/SwiftCLI

Swift Package Manager

Add SwiftCLI as a dependency to your project:

dependencies: [
    .package(url: "https://github.com/jakeheis/SwiftCLI", from: "6.0.0")
]

Carthage

github "jakeheis/SwiftCLI" ~> 5.2.2

CocoaPods

pod 'SwiftCLI', '~> 6.0.0'

Creating a CLI

When creating a CLI, a name is required, and a version and description are both optional.

let myCli = CLI(name: "greeter", version: "1.0.0", description: "Greeter - a friendly greeter")

You set commands through the .commands property:

myCli.commands = [myCommand, myOtherCommand]

Finally, to run the CLI, you call one of the go methods.

// Use go if you want program execution to continue afterwards
myCli.go() 

// Use goAndExit if you want your program to terminate after the CLI has finished
myCli.goAndExit()

// Use go(with:) if you want to control the arguments which the CLI runs with
myCli.go(with: ["arg1", "arg2"])

Commands

In order to create a command, you must implement the Command protocol. All that's required is to implement a name property and an execute function; the other properties of Command are optional (though a shortDescription is highly recommended). A simple hello world command could be created as such:

class GreetCommand: Command {

    let name = "greet"
    let shortDescription = "Says hello to the world"

    func execute() throws  {
        stdout <<< "Hello world!"
    }

}

Parameters

A command can specify what parameters it accepts through certain instance variables. Using reflection, SwiftCLI will identify property wrappers of type @Param and @CollectedParam. These properties should appear in the order that the command expects the user to pass the arguments. All required parameters must come first, followed by any optional parameters, followed by at most one collected parameter.

class GreetCommand: Command {
    let name = "greet"

    @Param var first: String
    @Param var second: String?
    @CollectedParam var remaining: [String]
}

In this example, if the user runs greeter greet Jack Jill up the hill, first will contain the value Jack, second will contain the value Jill, and remaining will contain the value ["up", "the", "hill"].

@Param

Individual parameters take the form of the property wrapper @Param. Properties wrapped by @Param can be required or optional. If the command is not passed enough arguments to satisfy all required parameters, the command will fail.

class GreetCommand: Command {
    let name = "greet"

    @Param var person: String
    @Param var followUp: String

    func execute() throws {
        stdout <<< "Hey there, \(person)!"
        stdout <<< followUp
    }
}
~ > greeter greet Jack

Usage: greeter greet <person> <followUp> [options]

Options:
  -h, --help      Show help information

Error: command requires exactly 2 arguments

~ > greeter greet Jack "What's up?"
Hey there, Jack!
What's up?

If the user does not pass enough arguments to satisfy all optional parameters, the value of these unsatisfied parameters will be nil.

class GreetCommand: Command {
    let name = "greet"

    @Param var person: String
    @Param var followUp: String? // Note: String? in this example, not String

    func execute() throws {
        stdout <<< "Hey there, \(person)!"
        if let followUpText = followUp {
            stdout <<< followUpText
        }
    }
}
~ > greeter greet Jack
Hey there, Jack!
~ > greeter greet Jack "What's up?"
Hello, Jack!
What's up?

@CollectedParam

Commands may have a single collected parameter after all the other parameters called a @CollectedParam. This parameter allows the user to pass any number of arguments, and these arguments will be collected into the array wrapped by the collected parameter. The property wrapped by @CollectedParam must be an array. By default, @CollectedParam does not require the user to pass any arguments. The parameter can require a certain number of values by using the @CollectedParam(minCount:) initializer.

class GreetCommand: Command {
    let name = "greet"

    @CollectedParam(minCount: 1) var people: [String]

    func execute() throws {
        for person in people {
            stdout <<< "Hey there, \(person)!"
        }        
    }
}
~ > greeter greet Jack
Hey there, Jack!
~ > greeter greet Jack Jill Water
Hey there, Jack!
Hey there, Jill!
Hey there, Water!

Value type of parameter

With all of these parameter property wrappers, any type can be used so long as it conforms to ConvertibleFromString. Most primitive types (e.g. Int) conform to ConvertibleFromString already, as do enums with raw values that are primitive types.

class GreetCommand: Command {
    let name = "greet"

    @Param var number: Int

    func execute() throws {
        stdout <<< "Hey there, number \(number)!"     
    }
}
~ > greeter greet Jack

Usage: greeter greet <number> [options]

Options:
  -h, --help      Show help information

Error: invalid value passed to 'number'; expected Int

~ > greeter greet 4
Hey there, number 4!

Parameters with enum types which conform to CaseIterable have additional specialized behavior. In an error message, the allowed values for that parameter will be spelled out.

class GreetCommand: Command {
    
    let name = "greet"
    
    enum Volume: String, ConvertibleFromString, CaseIterable {
        case loud
        case quiet
    }
    
    @Param var volume: Volume
    
    func execute() throws {
        let greeting = "Hello world!"
        
        switch volume {
        case .loud: stdout <<< greeting.uppercased()
        case .quiet: stdout <<< greeting.lowercased()
        }
        
    }
}
~ > greeter greet Jack

Usage: greeter greet <volume> [options]

Options:
  -h, --help      Show help information

Error: invalid value passed to 'volume'; expected one of: loud, quiet

~ > greet greet loud
HELLO WORLD!

To conform a custom type to ConvertibleFromString, simply implement one function:

extension MyType: ConvertibleFromString {
    init?(input: String) {
        // Construct an instance of MyType from the String, or return nil if not possible
        ...
    }
}

Options

Commands have support for two types of options: flag options and keyed options. Both types of options can be denoted by either a dash followed by a single letter (e.g. git commit -a) or two dashes followed by the option name (e.g. git commit --all). Single letter options can be cascaded into a single dash followed by all the desired options: git commit -am "message" == git commit -a -m "message".

Options are specified with property wrappers on the command class, just like parameters:

class ExampleCommand: Command {
    ...
    @Flag("-a", "--all")
    var flag: Bool

    @Key("-t", "--times")
    var key: Int?
    ...
}

Flags

Flags are simple options that act as boolean switches. For example, if you were to implement git commit, -a would be a flag option. They take the form of booleans wrapped by @Flag.

The GreetCommand could take a "loudly" flag:

class GreetCommand: Command {

    ...

    @Flag("-l", "--loudly", description: "Say the greeting loudly")
    var loudly: Bool

    func execute() throws {
        if loudly {
             ...
        } else {
            ...
        }
    }

}

A related option type is @CounterFlag, which counts the nubmer of times the user passes the same flag. @CounterFlag can only wrap properties of type Int. For example, with a flag declaration like:

class GreetCommand: Command {
    ...
    @CounterFlag("-s", "--softly", description: "Say the greeting softly")
    var softly: Int
    ...
}

the user can write greeter greet -s -s, and softly.value will be 2.

Keys

Keys are options that have an associated value. Using "git commit" as an example, "-m" would be a keyed option, as it has an associated value - the commit message. They take the form of variables wrapped by '@Key`.

The GreetCommand could take a "number of times" option:

class GreetCommand: Command {

    ...

    @Key("-n", "--number-of-times", description: "Say the greeting a certain number of times")
    var numberOfTimes: Int?

    func execute() throws {
        for i in 0..<(numberOfTimes ?? 1) {
            ...
        }
    }

}

The variable wrapped by @Key can be any type conforming to ConvertibleFromString as described above. It must be optional, or the Swift compiler will crash.

A related option type is VariadicKey, which allows the user to pass the same key multiples times with different values. For example, with a key declaration like:

class GreetCommand: Command {
    ...
    @VariadicKey("-l", "--location", description: "Say the greeting in a certain location")
    var locations: [String]
    ...
}

the user can write greeter greet -l Chicago -l NYC, and locations.value will be ["Chicago", "NYC"]. The variable wrapped by @VariadicKey must be an array of a type conforming to ConvertibleFromString.

Option groups

The relationship between multiple options can be specified through option groups. Option groups allow a command to specify that the user must pass at most one option of a group (passing more than one is an error), must pass exactly one option of a group (passing zero or more than one is an error), or must pass one or more options of a group (passing zero is an error).

To add option groups, a Command should implement the property optionGroups. Option groups refer to options through the $ syntax. For example, if the GreetCommand had a loudly flag and a whisper flag but didn't want the user to be able to pass both, an OptionGroup could be used:

class GreetCommand: Command {

    ...

    @Flag("-l", "--loudly", description: "Say the greeting loudly")
    var loudly: Bool

    @Flag("-w", "--whisper", description: "Whisper the greeting")
    var whisper: Bool
    
    var optionGroups: [OptionGroup] {
        return [.atMostOne($loudly, $whipser)] // Note: $loudly and $whisper, not loudly and whisper
    }

    func execute() throws {
        if loudly {
            ...
        } else if whisper {
            ...
        } else {
            ...
        }
    }

}

Global options

Global options can be used to specify that every command should have a certain option. This is how the -h flag is implemented for all commands. Simply add an option to CLI's .globalOptions array (and optionally extend Command to make the option easy to access in your commands):

private let verboseFlag = Flag("-v")
extension Command {
    var verbose: Bool {
        return verboseFlag.value
    }
}

myCli.globalOptions.append(verboseFlag)

By default, every command has a -h flag which prints help information. You can turn this off by setting the CLI helpFlag to nil:

myCli.helpFlag = nil

Usage of options

As seen in the above examples, @Flag and @Key both take an optional description parameter. A concise description of what the option does should be included here. This allows the HelpMessageGenerator to generate a fully informative usage statement for the command.

A command's usage statement is shown in three situations:

  • The user passed an option that the command does not support -- greeter greet -z
  • The user passed the wrong number of arguments
  • The command's help was invoked -- greeter greet -h
~ > greeter greet -h

Usage: greeter greet <person> [options]

Options:
  -l, --loudly                          Say the greeting loudly
  -n, --number-of-times <value>         Say the greeting a certain number of times
  -h, --help                            Show help information for this command

Command groups

Command groups provide a way for related commands to be nested under a certain namespace. Groups can themselves contain other groups.

class ConfigGroup: CommandGroup {
    let name = "config"
    let children = [GetCommand(), SetCommand()]
}
class GetCommand: Command {
    let name = "get"
    func execute() throws {}
}
class SetCommand: Command {
    let name = "set"
    func execute() throws {}
}

You can add a command group to your CLI's .commands array just as add a normal command:

greeter.commands = [ConfigGroup()]
> greeter config

Usage: greeter config <command> [options]

Commands:
  get
  set

> greeter config set
> greeter config get

Shell completions

Zsh completions can be automatically generated for your CLI.

let myCli = CLI(...)

let generator = ZshCompletionGenerator(cli: myCli)
generator.writeCompletions()

Completions will be automatically generated for command names and options. Parameter completion mode can be specified:

@Param(completion: .none)
var noCompletions: String

@Param(completion: .filename)
var aFile: String

@Param(completion: .values([
    ("optionA", "the first available option"),
    ("optionB", "the second available option")
]))
var aValue: String

@Param(completion: .function("_my_custom_func"))
var aFunction: String

The default parameter completion mode is .filename. If you specify a custom function with .function, that function must be supplied when creating the completion generator:

class MyCommand {
    ...
    @Param(completion: .function("_list_processes"))
    var pid: String
    ...
}

let myCLI = CLI(...)
myCLI.commands [MyCommand()]
let generator = ZshCompletionGenerator(cli: myCli, functions: [
    "_list_processes": """
        local pids
        pids=( $(ps -o pid=) )
        _describe '' pids
        """
])

Built-in commands

CLI has two built-in commands: HelpCommand and VersionCommand.

Help Command

The HelpCommand can be invoked with myapp help. The HelpCommand first prints the app description (if any was given during CLI.init). It then iterates through all available commands, printing their name and their short description.

~ > greeter help

Usage: greeter <command> [options]

Greeter - your own personal greeter

Commands:
  greet        Greets the given person
  help         Prints this help information

If you don't want this command to be automatically included, set the helpCommand property to nil:

myCLI.helpCommand = nil

Version Command

The VersionCommand can be invoked with myapp version or myapp --version. The VersionCommand prints the version of the app given during init CLI(name:version:). If no version is given, the command is not available.

~ > greeter --version
Version: 1.0

If you don't want this command to be automatically included, set the versionCommand property to nil:

myCLI.versionCommand = nil

Input

The Input class makes it easy to read input from stdin. Several methods are available:

let str = Input.readLine()
let int = Input.readInt()
let double = Input.readDouble()
let bool = Input.readBool()

All read methods have four optional parameters:

  • prompt: the message to print before accepting input (e.g. "Input: ")
  • secure: if true, the input is hidden as the user types
  • validation: a closure which defines whether the input is valid, or if the user should be reprompted
  • errorResponse: a closure which is executed when the user enters input which is not valid

For example, you could write:

let percentage = Input.readDouble(
    prompt: "Percentage:",
    validation: [.within(0...100)],
    errorResponse: { (input, reason) in
        Term.stderr <<< "'\(input)' is invalid; must be a number between 0 and 100"
    }
)

which would result in an interaction such as:

Percentage: asdf
'asdf' is invalid; must be a number between 0 and 100
Percentage: 104
'104' is invalid; must be a number between 0 and 100
Percentage: 43.6

External tasks

SwiftCLI makes it easy to execute external tasks:

// Execute a command and print output:
try Task.run("echo", "hello")
try Task.run(bash: "while true; do echo hi && sleep 1; done")

// Execute a command and capture the output:
let currentDirectory = try Task.capture("pwd").stdout
let sorted = try Task.capture(bash: "cat Package.swift | sort").stdout

You can also use the Task class for more custom behavior:

let input = PipeStream()
let output = PipeStream()
let task = Task(executable: "sort", currentDirectory: "~/Ice", stdout: output, stdin: input)
task.runAsync()

input <<< "beta"
input <<< "alpha"
input.closeWrite()

output.readAll() // will be alpha\nbeta\n

See Sources/SwiftCLI/Task.swift for full documentation on Task.

Single command CLIs

If your CLI only contains a single command, you may want to execute the command simply by calling cli, rather than cli command. In this case, you can create your CLI as such:

class Ln: Command {
    let name = "ln"
    func execute() throws { ... }
}

let ln = CLI(singleCommand: Ln())
ln.go()

In this case, if the user writes ln myFile newLocation, rather than searching for a command with the name "myFile", SwiftCLI will execute the Ln command and pass on "myFile" as the first argument to that command.

Keep in mind that when creating a single command CLI, you lose the default VersionCommand. This means that cli -v will not work automatically, and that if you want to print your CLI version you will need to manually implement a Flag("-v") on your single command.

Customization

SwiftCLI was designed with sensible defaults but also the ability to be customized at every level. CLI has three properties that can be changed from the default implementations to customized implementations.

parser

The Parser steps through arguments to find the corresponding command, update its parameter values, and recognizes options. Each CLI has a parser property which has three mutable properties: routeBehavior, parseOptionsAfterCollectedParameter, and responders.

routeBehavior has three possible values. The default, .search, steps through the arguments the user passed and tries to find a command with a matching name. If it fails, a help message is printed. git is an example of a program which operates this way. The second option, .searchWithFallback(Command), also initially tries to find a command with a name matching the arguments passed by the user, but if it fails, rather than printing a help message it falls back to a certain command. 'bundle' is an example of a program which operates this way. The last option is .automatically(Command). In this route behavior, the CLI automatically routes to the given command without considering arguments. 'ln' is an example of a program which operates this way.

parseOptionsAfterCollectedParameter controls whether or not options are recognized once a collected parameter is encountered. It defaults to false. Given the following command:

class Run: Command {
    let name = "run"

    @Param var executable: String
    @CollectedParam var arguments: [String]

    @Flag("-a") var all: Bool
    ...
}

if the user calls swift run myExec -a and parseOptionsAfterCollectedParameter is false, the value of executable will be "myExec", the value of arguments will be ["-a"], and the value of all will be false. If parseOptionsAfterCollectedParameter were true in this situation, the value of executable would be "myExec", the value of arguments would be [], and the value of all would be true.

responders allows the parser to be completely customized. Check out Parser.swift for more information on how this functions by default and how it can be customized in any way.

aliases

Aliases can be made through the the aliases property on CLI. Parser will take these aliases into account while routing to the matching command. For example, if you write:

myCLI.aliases["-c"] = "command"

And the user makes the call myapp -c, the parser will search for a command with the name "command" because of the alias, not a command with the name "-c".

By default, "--version" is an alias for "version", but you can remove this if desired:

myCLI.aliases["--version"] = nil

argumentListManipulators

ArgumentListManipulators act before the Parser begins. They take in the arguments as given by the user and can change them slightly. By default, the only argument list manipulator used is OptionSplitter which splits options like -am into -a -m.

You can implement ArgumentListManipulator on your own type and update CLI's property:

cli.argumentListManipulators.append(MyManipulator())

helpMessageGenerator

The messages formed by SwiftCLI can also be customized:

cli.helpMessageGenerator = MyHelpMessageGenerator()

Running your CLI

Simply call swift run. In order to ensure your CLI gets the arguments passed on the command line, make sure to call CLI.go(), not CLI.go(with: []).

CLIs build with SwiftCLI

swiftcli's People

Contributors

antowkos avatar ericrabil avatar erikprice avatar filipbec avatar fredloh avatar giginet avatar jacobbudin avatar jakeheis avatar jeehut avatar larsschwegmann avatar lukaskubanek avatar matrixsenpai avatar mroth avatar msanders avatar nxsoftware avatar rbukovansky avatar tamaracha avatar thecoordinator avatar udumft avatar victor avatar yonaskolb avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swiftcli's Issues

Tag for latest changes

@jakeheis could we please get a tag on last commit? 2.0.1-beta1 or 2.0.1 or anything like this? SwiftPM is not able to pickup the changes until it's tagged. Thanks a lot.

Help command argument

It would be great if the inbuilt help command accepted a command argument to get help on subcommands, as an alternative to --help

tool command --help # currently works
tool help command # new way
tool help command subcommand # could even walk the command tree

I can look at this when I have some free time

debugGoWithArgumentString() does not properly handle quoted arguments

Given the following command:

CLI.registerChainableCommand(commandName: "greet")
    .withShortDescription("Greets the given person")
    .withSignature("<person>")
    .withExecutionBlock { (arguments) in 
        let person = arguments.requiredArgument("person");
        print("Hey there, \(person)!");
    }
;

When using CLI.go(), passing quoted arguments properly parses them as single arguments. Here is the result of using it in an app and passing a quoted string:

$ myApp greet "Bryan Forbes"
Hey there, Bryan Forbes!

However, when using CLI.debugGoWithArgumentSring("myApp greet \"First Name, Last Name\""), an error is raised:

[Debug Mode]
Expected 1 argument, but got 2.

The reason is that CLI.debugGoWithArgumentSring() splits on spaces without considering quotes which means "Bryan Forbes" ends up as two arguments where one is "Bryan and the other is Forbes".

Commands with Flags and CollectedParameters don't work as expected

I have a command like this:

class HelloCommand: Command {
    let name = "hello"
    let json = Flag("-j", "--json")
    let files = CollectedParameter()

    func execute() throws {
        print("\(files.value)")
    }
}

However, whenever I pass the --json argument it is just grouped into files, regardless of where it is placed. Maybe this is a known limitation or maybe I am using it wrong, but it feels like a parser error. If I replace files with a regular Parameter it works just fine.

The rest of the framework is great though, very intuitive and easy to use. Thanks for your work on this.

Update Swift 4 branch

Should the swift_4 branch be updated with the changes from master? I would like to make a Swift 4-related pull request to fix deprecation warnings such as:

warning: 'characters' is deprecated: Please use String or Substring directly

I assume I should make the pull request to this branch. If I'm wrong, let me know. Thanks.

CLI without "command" (in version 5.0)

I was reading the prior question about how to have a CLI with only one command that doesn't need to be passed, and saw this answer:

#55 (comment)

However, my CLI object in 5.0 doesn't have a router property. Is there a new way to do this?

Global options

I'm trying to figure it out, but I have failed...
I would like to have global options for my application, i.e. -v/--verbose for verbose response and -h/--help for help of specified command (-v and -h without command can default to version and help).
Am I able do it using SwiftCLI?

Thanks for help.

`Open` Flag class

Setting the Flag class (and some others I imagine) to open, instead of public, could enable nice things.
In my case it would allow me to handle a global --no-color flag way more nicely:

import ColorizeSwift

class GlobalFlag: Flag {
	typealias Action = () -> Void
	let flagAction: Action?

	init(_ name: String, usage: String, defaultValue: Bool = false, action: Action? = nil) {
		flagAction = action
		super.init(name, usage: usage, defaultValue: defaultValue)
	}

	override func setOn() {
		flagAction?()
		super.setOn()
	}
}

// Allows this
struct ColorizationOptions: GlobalOptionsSource {
	let ignoreColorization = GlobalFlag("--no-color", usage: "Disable output colorization") {
		String.isColorizationEnabled = false
	}

	static var options: [Option] {
		return [ignoreColorization]
	}
}

GlobalOptions.source(ColorizationOptions.self)

Parameter descriptions

It might be nice to be able to give a Parameter a description so it can be show in the usage output for the command. Perhaps this can be under a Parameters list similar to how Options is shown.
You could argue if the parameter is required the usage info could go into the command description, but at the moment the usage for a command doesn't show it's own description, it only does in the command list

Usage with CocoaPods

Hello,

Wonderfull library for building CLIs.

The installation instructions are only for the Swift Package Manager.

Is it possible to add it as a dependency using cocoa pods?

ENV for execvp

Having the ability to process input via running with Task.execvp is great!
I need to be able to pass environment variables to that process though. Is that possible?

Idiomatic way to invoke a command from another command

I did a quick search on previous issues and I think what I want to accomplish might not be a good idea (since nobody apparently thought about this before), but is there a way to chain command invocations (extra: passing arguments) with SwiftCLI?

What I ideally would like to accomplish is something like this:

class Command1: Command {
    func execute() throws {
        ...
        var command2 = Command2()
        command2.param = "param" //this gives a 'param' is get-only error
        try command2.execute()
    }
    ...
}

class Command2: Command {
    @Param var param: String?

    func execute() throws {
        ...
    }
    ...
}

Key and Param default value

It would be awesome to have an optional default value for Param and Key. This would allow:

  • setting the default value in the same place as the rest of the option configuration, instead of down in execute with option ?? defaultValue
  • allow Param and Key to have non optional types (Param already supports non optional types but currently fails if value not supplied)
  • allow an option to HelpMessageGenerator that would print the default value of options, perhaps after the description eg: " Defaults to \(defaultValue)"
@Key("--type", "-t", description: "The type of dump to output", defaultValue: .yaml)
private var dumpType: DumpType

The reason why I actually wanted to be able to customize Command Signature.

I really like SwiftCLI but there's a couple non-trivial features missing in order to make truly complex CLIs.

  1. The ability to group parameters (and options) in groups, and subsequently specify how many members of a group we are expecting (i.e. Exactly one, At least one, At most one, all of them) or alternatively a way to specify that certain parameters or options conflict with each other.

  2. The ability to make parameters that also require a value, like keyed options do.

For example, in my use case, before I began looking into the whole changing the Command Signature issue, I ended up using commands with signature "" and all of their functionality implemented through options and flags., since I found that was the only way I could avoid issues caused by mismatching signature parameter counts.
Although what I did to aliases would probably greatly offset this issue, it's still not really an elegant solution to use extensively.

Add Carthage compatibility

While SwiftCLI is designes for Swift Package Manager, some tools might want to (also) integrate it via Carthage. Adding such support should be faily simple by just creating and checking in an Xcode project with a shared scheme. As for maintenance, the Xcode project would need to be updated every time there is a new file, other than that there should be few work to do if any.

Usage description is incorrect for SingleCommand

If using a SingleCommandRouter the help usage contains the command name.

Usage: tool commandName <parameter> [options]

Even if the command.name is set to "" there are 2 spaces

Usage: tool  <parameter> [options]

Cannot be used with Cocoapods

This issue is not really an issue with SwiftCLI, but with the current developer tools state. But I wanted to document it anyway for future users of the library.

I was trying to create the Cocoapods' podspec so that I could link to the library as a 3rd party framework. After 2 days hitting into a wall, I realized that it is not currently possible to link to dynamic libraries (or frameworks) from command line apps, because they are single file apps and lack the necessary bundle structure.

There is a cumbersome mechanism for using a bundle that runs inside a command line tool and provides the 3rd party frameworks, but it is too complex to be used with Cocoapods anyway.

I have opened a radar (rdar://20250458) about this issue.

So, for now, even though Cocoapods supports Swift frameworks, the recommended way to install the library is to copy it the source files. (Of course, if you were not building a command line app, there would be no problem. But then, what do you need a command line parser for?)

Update README in swift-3.0-beta5 branch

Hi,
could you please update README.md in swift-3.0-beta5 branch how to use SwiftCLI for Swift 3.0? I'm trying to build my swift 3.0 app, but with standard package configuration I'm getting tons of error during swift build for SwiftCLI package.

Thank you.

Consider: Supporting @env: syntax for reading options.

A common idiom in command line apps is to support reading arguments from the environment. This is particularly useful in CI/CD situations where often config items or secrets are passed via environment variables. Eg:

mycommand action --apiKey @env:API_KEY

This style of passing arguments can also help when the content of the env key might cause command parsing issues (eg multiline values).

make name of CLI optional and use argv[0] as default

Have you considered to make name in the constructor of CLI optional and default to the name of the executable (argv[0])? This would help developing simple scripts, where the user may rename the script or executable which would then also be reflected in the help message.

Variadic options

Would be great to have support for variadic options

command --option value1 --option value2

Allow changing newline padding in usage messages

It would be nice if there were a way to disable the newline padding in usage and error messages without re-implementing the default implementations in HelpMessageGenerator. I personally think it makes sense to turn it off by default, since that's more consistent with other command line tools. E.g.:


Usage: cli <command> [options]

Commands:
  help              Prints help information
  version           Prints the current version of this app

would become:

Usage: cli <command> [options]

Commands:
  help              Prints help information
  version           Prints the current version of this app

Having Global Option Groups

Not sure if this is possible or not, but it would be super convenient if one could use Option Groups as Global Options.

I'm creating a tool where all commands require the same mutually exclusive Key items to define the input source (different possible variations of sources, but there must be one and only one).

What I am doing now is for each command, I'm doing this:

    @Key("-p", "--project")
    var project: String?
    @Key("-w", "--workspace")
    var workspace: String?
    @Key("-f", "--file")
    var file: String?
    var optionGroups: [OptionGroup] {
        return [.exactlyOne($project, $workspace, $file)]
    }

I'd ideally like to make that global.

Standard In

Is launching a process via a Task and then having that read from standard in supported? It doesn't seem to work by default.

'capture' func catch fatal error.

Fatal error: Couldn't parse data into string using Unicode (UTF-8): file /tmp/build/Course/checkouts/SwiftCLI/Sources/SwiftCLI/Stream.swift, line 213 2019-05-29 21:45:35.852414+0800 Course[2522:79439] Fatal error: Couldn't parse data into string using Unicode (UTF-8): file /tmp/build/Course/checkouts/SwiftCLI/Sources/SwiftCLI/Stream.swift, line 213 Program ended with exit code: 9

    private static func captureCurl(args: [String], timeout: Int? = nil) throws -> String {
        return try capture("curl", arguments: args).rawStdout
    }

add support for --version in built-in version command.

I like the built-in version-command. Consider to add support for the pretty standard --version flag.

I do prefer to use this flag quite often if I want to check for a version of a command, where I can't remember the exact syntax to use. Since the short flag -v sometimes refers to "verbose", I try to avoid using -v.

I can't find command

~ > greeter greet Jack
Expected 2 arguments, but got 1.

my terminal is $, not >, I use SwiftCLI by SwiftPM, but I can't find command greeter greet ,
Where did I go wrong? and I need to install Ice ?

False Flag

It would be great to be able to set a flag to false.
eg: --flag=false

Stream observer

In regards to the new stream functionality in SwiftCLI 4.2.0, what's the best way to filter/forward a stream? For example taking what is written to stdout in swift build and then somehow filtering it and re-outputting it?

Would that be a subclass of WriteStream that overrides write and forwards it on to another stream, or some sort of observation mechanism on Pipe, or is there a better way?

Questions: Idiomatic way to specify default command

Is there a way to specify a command to run if one is not given on the cmd line?
Right now I am hijacking Command.arguments to do this.
Something like this:

let args = CommandLine.arguments
let cli = MyCli()
if args.count > 1 {
    cli.run(Array(args.dropFirst()))
} else {
    cli.run(["default"])
}

run then calls

cli.go(with: args)

Is there a better way?

Tests fail in Xcode

Tests pass via swift test, but fail when run in Xcode due to the current directory not being in the root of the repo.
For example TaskTests.testCapture.
This isn't a big deal, but it's sometimes nice to use Xcode for editing the project and being able to run a single test easily

Question: CLIs without "command"

Is there support for CLIs without a specific command? If so, some hints to that in the README would be helpful.

In detail, if I want to develop a simple command line tool, which takes some flags and some files as input, e.g.

foo -a bar.txt baz.log -f

I do not have a command. Instead I have just some flags -a and -f, plus and arbitrary number of optional collected parameters.
I good real world example of such a tool is the famous rm from the GNU core utils, which, unlike the macOS version of rm accepts flags at any position.

Question: Idiomatic way to restrict values of parameter to enum and surface that to user

I'm looking for the idiomatic way to restrict values of parameter to enum and surface that to user. Apologies if this is not the right way to surface this question.

For example, if there are only 3 supported values for a required parameter, what's the best way to surface that info to the user? If there is no idiomatic way, can we treat this as a feature request?

Help on Error

An option to display the help command if there is an error or another command throws

Consider adding a public changelog

As a user it's difficult now to keep track of what's changed between releases. This would be easier with a public changelog file, or alternatively notes on the GitHub releases page.

[FeatReq] Subcommands

@jakeheis For my project I will need to have what we can call subcommands or application areas. Most of the time user will need, due to complexity of application, to specify "area" he wants to work within and then command for that area. Similar to git remote command. Let me better explain in CLI commands:

Code area:
$ cx code add readme.md
$ cx code commit "Init commit"
$ cx code status

Issues area:
$ cx issue add "Something is broken" <-- different add to add in Code area
$ cx issue show 1234
$ cx issue resolve 1234

Build area:
$ cx build run
$ cx build restart

etc.

Do you think it would be possible to add it to SwiftCLI? Thank you.

"too many arguments (4468) -- limit is 4096" error

I'm just rebasing my tool BartyCrouch to use SwiftCLI but after making the switch on first try on one of our larger projects, I came across this error:

2018-12-13 17:34:49.112 bartycrouch[12963:3427202] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'too many arguments (4468) -- limit is 4096'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff35bb6e65 __exceptionPreprocess + 256
	1   libobjc.A.dylib                     0x00007fff61c12720 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff35bb6c97 +[NSException raise:format:] + 201
	3   Foundation                          0x00007fff37eabaf3 -[NSConcreteTask launchWithDictionary:error:] + 774
	4   bartycrouch                         0x000000010371373a $S8SwiftCLI4TaskC6launch030_19E0ACB72F1C972020BFBD69850F9J1FLLyyF + 506
	5   bartycrouch                         0x0000000103713249 $S8SwiftCLI4TaskC7runSyncs5Int32VyF + 25
	6   bartycrouch                         0x0000000103716626 $S8SwiftCLI3run_9arguments9directoryySS_SaySSGSSSgtKFTf4xnn_n + 390
	7   bartycrouch                         0x0000000103712669 $SAbort trap: 6

Looks like there's an arguments limit to tasks executed in the command line. Can this be fixed within this library (easiliy) or do I need to build a wrapper around the run(:arguments:) method and run my command multiple times until it has worked through all arguments?

The usage code is probably this line in case you're wondering.

Arbitrary arguments

This library looks fantastic, nice work @jakeheis!

Does it support the ability to parse arbitrary arguments? I'll give an example:

mint run jakeheis/ice ice add RxSwift

mint is the executable
run is a command in mint
jakeheis/ice is an argument in run
The rest are arbitrary arguments that will need to be parsed somehow for later use

If there's some way to do that, I'd love to integrate this

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.