Documentation ¶
Overview ¶
Package cmdy is a minimalistic library for implementing CLI programs.
cmdy combines the features I like from the `flag` stdlib package with the features I like from https://github.com/google/subcommands.
cmdy focuses on minimalism and tries to imitate and leverage the stdlib as much as possible. It does not attempt to replace `flag.Flag`, though it does extend it slightly.
cmdy does not prioritise performance (beyond the fact that Go is already pretty fast out of the box).
Example (Maingroup) ¶
package main import ( "context" "fmt" "github.com/ArtProcessors/cmdy" "github.com/ArtProcessors/cmdy/arg" ) type mainGroupCommand struct{} func newMainGroupCommand() cmdy.Command { return &mainGroupCommand{} } func (cmd *mainGroupCommand) Synopsis() string { return "Example main group command" } func (cmd *mainGroupCommand) Configure(flags *cmdy.FlagSet, args *arg.ArgSet) {} func (cmd *mainGroupCommand) Run(ctx cmdy.Context) error { fmt.Fprintf(ctx.Stdout(), "subcommand!\n") return nil } func main() { // Ignore this, pretend it isn't here. cmdy.Reset() // builders allow multiple instances of the command to be created. mainBuilder := func() cmdy.Command { // flag values should be scoped to the builder: var testFlag bool return cmdy.NewGroup( "myprog", cmdy.Builders{ // Add your subcommand builders here. This has the same signature as // mainBuilder - you can nest cmdy.Groups arbitrarily. "mycmd": newMainGroupCommand, }, // Optionally override the default usage: cmdy.GroupUsage("Usage: yep!"), // Optionally provide global flags. Flags are left-associative in cmdy, so // any flag in here must come before the subcommand: cmdy.GroupFlags(func() *cmdy.FlagSet { set := cmdy.NewFlagSet() set.BoolVar(&testFlag, "testflag", false, "test flag") return set }), // Optionally provide global setup code to run before the Group's // subcommand is created and run: cmdy.GroupBefore(func(ctx cmdy.Context) error { fmt.Fprintf(ctx.Stdout(), "before! flag: %v\n", testFlag) return nil }), // Optionally provide global teardown code to run after the Group's // subcommand is run: cmdy.GroupAfter(func(ctx cmdy.Context, err error) error { fmt.Fprintf(ctx.Stdout(), "after! flag: %v\n", testFlag) // Careful: failing to return the passed in error here will // swallow the error. return err }), ) } args := []string{"-testflag", "mycmd"} if err := cmdy.Run(context.Background(), args, mainBuilder); err != nil { cmdy.Fatal(err) } }
Output: before! flag: true subcommand! after! flag: true
Index ¶
- Constants
- Variables
- func CommandPath(ctx Context) (out []string)
- func ErrWithCode(code int, err error) error
- func Fatal(err error)
- func FormatError(err error) (msg string, code int)
- func HelpRequest() error
- func IsUsageError(err error) bool
- func ProgName() string
- func ReaderIsPipe(in io.Reader) bool
- func Reset()
- func Run(ctx context.Context, args []string, b Builder) (rerr error)
- func UsageError(err error) error
- func UsageErrorf(msg string, args ...interface{}) error
- func WriterIsPipe(out io.Writer) bool
- type BufferedRunner
- type Builder
- type Builders
- type Command
- type CommandArgs
- type CommandFlags
- type CommandRef
- type Context
- type Error
- type FlagSet
- type Group
- type GroupOption
- func GroupAfter(fn func(Context, error) error) GroupOption
- func GroupBefore(before func(Context) error) GroupOption
- func GroupFlags(fb func() *FlagSet) GroupOption
- func GroupHide(names ...string) GroupOption
- func GroupMatcher(cm Matcher) GroupOption
- func GroupPrefixMatcher(minLen int) GroupOption
- func GroupRewrite(rw GroupRewriter) GroupOption
- func GroupUsage(usage string) GroupOption
- type GroupRewriter
- type GroupRunState
- type Matcher
- type QuietExit
- type Runner
- type Usage
- type UsageCommand
Examples ¶
Constants ¶
const ( ExitSuccess = 0 ExitFailure = 1 ExitUsage = 127 ExitInternal = 255 )
FIXME: these are Unix codes, but other operating systems use different codes.
On macOS/Linux, it looks like Go uses status code 2 for a panic, so it's probably a good idea to avoid that. Discussion will be on one or both of these threads:
https://groups.google.com/forum/#!msg/golang-nuts/u9NgKibJsKI/XxCdDihFDAAJ https://github.com/golang/go/issues/24284
const DefaultUsage = `
{{if Synopsis -}}
{{Synopsis}}
{{end -}}
Usage: {{Invocation}}
`
DefaultUsage is used to generate your usage string when your Command does not implement cmdy.Usage. You can prepend it to your own usage templates if you want to add to it:
const myCommandUsage = cmdy.DefaultUsage + "\n"+ ` Extra stuff about my command that will be stuck on the end. Etc etc etc. ` func (c *myCommand) Usage() string { return myCommandUsage }
Variables ¶
var FlagDoubleDash = false
FlagDoubleDash allows you to globally configure whether long flag names will show in the help message with two dashes or one. This is to appease those who are (not unreasonably) uncomfortable with the fact that the single dash longopt direction the Go team decided to take is totally out of step with the entire Unix world around us.
Functions ¶
func CommandPath ¶
func ErrWithCode ¶
ErrWithCode allows you to wrap an error in a status code which will be used by cmdy.Fatal() as the exit code.
func Fatal ¶
func Fatal(err error)
Fatal prints an error formatted for the end user using the global DefaultRunner, then calls os.Exit with the exit code detected in err.
Calls to Fatal() will prevent any defer calls from running. This pattern is strongly recommended instead of a straight main() function:
func main() { if err := run(); err != nil { cmdy.Fatal(err) } } func run() error { // your command in here return nil }
func FormatError ¶
func HelpRequest ¶
func HelpRequest() error
HelpRequest wraps an existing error so that cmdy.Fatal() will print the full command help.
func IsUsageError ¶
func ProgName ¶
func ProgName() string
ProgName attempts to guess the program name from the first argument in os.Args.
func ReaderIsPipe ¶
ReaderIsPipe probably returns true if the input is receiving piped data from another program, rather than from a terminal.
This is known to work in the following environments:
- Bash on macOS and Linux - Command Prompt on Windows - Windows Powershell
Typical usage:
if ReaderIsPipe(os.Stdin) { // Using stdin directly } if ReaderIsPipe(ctx.Stdin()) { // Using cmdy.Context }
func Run ¶
Run the command built by Builder b using the DefaultRunner, passing in the provided args.
The args should not include the program; if using os.Args, you should pass 'os.Args[1:]'.
The context provided should be your own master context; this allows global shutdown or cancellation to be propagated (provided your command blocks on APIs that support contexts). If no context is available, use context.Background().
func UsageError ¶
UsageError wraps an existing error so that cmdy.Fatal() will print the full command usage above the error message.
func UsageErrorf ¶
UsageErrorf formats an error message so that cmdy.Fatal() will print the full command usage above it.
func WriterIsPipe ¶
WriterIsPipe probably returns true if the Writer represents a pipe to another program, rather than to a terminal.
This is known to work in the following environments:
- Bash on macOS and Linux - Command Prompt on Windows - Windows Powershell
Typical usage:
if IsWriterPipe(os.Stdout) { // Using stdout directly } if IsWriterPipe(ctx.Stdin()) { // Using cmdy.Context }
Types ¶
type BufferedRunner ¶
type BufferedRunner struct { Runner StdinBuffer bytes.Buffer StdoutBuffer bytes.Buffer StderrBuffer bytes.Buffer }
func NewBufferedRunner ¶
func NewBufferedRunner() *BufferedRunner
NewBufferedRunner returns a Runner that wires Stdin, Stdout and Stderr up to bytes.Buffer instances.
type Builder ¶
type Builder func() Command
Builder creates an instance of your Command. The instance returned should be a new instance, not a recycled instance, and should only contain static dependency values that are cheap to create:
var goodBuilder = func() cmdy.Command { return &MyCommand{} } var goodBuilder = func() cmdy.Command { return &MyCommand{SimpleDep: "hello"} } var badBuilder = func() cmdy.Command { body, _ := http.Get("http://example.com") return &MyCommand{Stuff: body} } cmd := &MyCommand{} var badBuilder = func() cmdy.Command { return cmd }
type CommandArgs ¶
type CommandArgs interface { Command // Args defines positional arguments for your command. If you want to accept // all args, use github.com/ArtProcessors/cmdy/arg.All(). If no ArgSet is // returned, any arguments will cause an error. Args() *arg.ArgSet }
CommandArgs allows you to override the construction of the ArgSet in your Command. If your command does not implement this, it will receive a fresh instance of arg.ArgSet.
type CommandFlags ¶
type CommandFlags interface { Command // Flag definitions for your command. May return nil. If no FlagSet is // returned, --help is still supported but all other flags will cause an // error. Flags() *FlagSet }
CommandFlags allows you to override the construction of the FlagSet in your Command. If your command does not implement this, it will receive a fresh instance of cmdy.FlagSet.
type CommandRef ¶
FIXME: this name is not good
type Context ¶
type Context interface { context.Context RawArgs() []string Stdin() io.Reader Stdout() io.Writer Stderr() io.Writer Runner() *Runner Stack() []CommandRef Current() CommandRef Push(name string, cmd Command) Pop() (name string, cmd Command) }
Context implements context.Context; Context is passed into all commands when they are Run().
If you want your Command to be testable, you should access Stdin, Stdout and Stderr via Context.
type FlagSet ¶
FlagSet is a cmdy specific extension of flag.FlagSet; it is intended to behave the same way but with a few small extensions for the sake of this library. You should use it instead of flag.FlagSet when dealing with cmdy (though you can wrap an existing flag.FlagSet with FlagSetFromStd easily).
func FlagSetFromStd ¶
FlagSetFromStd wraps an existing flag.FlagSet instance and ensures it is correctly configured for use with cmdy.
The FlagSet will be modified. Custom output handlers are removed and the error handling is changed to ContinueOnError.
func NewFlagSet ¶
func NewFlagSet() *FlagSet
func (*FlagSet) HideUsage ¶
func (fs *FlagSet) HideUsage()
HideUsage prevents the "Flags" section from appearing in the Usage string.
func (*FlagSet) Invocation ¶
Invocation string for the flags, for example '[-foo=<yep>] [-bar=<pants>]`. If there are too many flags, `[options]` is returned instead.
type Group ¶
type Group struct { // Builders contains mappings between command names (received as the first // argument to this command) and the builder to delegate to. // // All Builders in this map will be called in order to create the Usage // string. Builders Builders // Allows interception of command strings so you can rewrite them to // other commands. Useful for aliases or handling empty arguments // very differently. Rewriter GroupRewriter Before func(Context) error After func(Context, error) error FlagBuilder func() *FlagSet Matcher Matcher // contains filtered or unexported fields }
Group implements a command that delegates to a subcommand. It selects a single Builder from a list of Builders based on the value of the first non-flag argument.
Example (PrefixMatcher) ¶
builders := Builders{ "foo": newFooCommand, "food": newFoodCommand, "bale": newBaleCommand, "bark": newBarkCommand, } bldr := func() Command { return NewGroup("group", builders, GroupPrefixMatcher(2)) } Run(context.Background(), []string{"foo"}, bldr) Run(context.Background(), []string{"food"}, bldr) Run(context.Background(), []string{"bar"}, bldr) Run(context.Background(), []string{"bal"}, bldr)
Output: foo food bark bale
type GroupOption ¶
type GroupOption func(cs *Group)
func GroupAfter ¶
func GroupAfter(fn func(Context, error) error) GroupOption
GroupAfter provides a function to call After a Group's subcommand is executed.
Any error returned by the subcommand is passed to the function. If it is not returned, it will be swallowed.
func GroupBefore ¶
func GroupBefore(before func(Context) error) GroupOption
GroupBefore provides a function to call Before a Group's subcommand is executed.
Any error returned by the before function will prevent the subcommand from executing.
func GroupFlags ¶
func GroupFlags(fb func() *FlagSet) GroupOption
GroupFlags provides a function that creates a FlagSet to the Group. This function may return nil. The result of this function may be cached.
func GroupHide ¶
func GroupHide(names ...string) GroupOption
GroupHide hides the builders from the usage string. If the builder does not exist in Builders, it will panic.
func GroupMatcher ¶
func GroupMatcher(cm Matcher) GroupOption
func GroupPrefixMatcher ¶
func GroupPrefixMatcher(minLen int) GroupOption
func GroupRewrite ¶
func GroupRewrite(rw GroupRewriter) GroupOption
func GroupUsage ¶
func GroupUsage(usage string) GroupOption
GroupUsage provides the usage template to the Group. The result of this function may be cached.
type GroupRewriter ¶
type GroupRewriter func(group *Group, args GroupRunState) (out *GroupRunState)
type GroupRunState ¶
type GroupRunState struct { // Builder of the subcommand to be run. May be nil if none was found for // the Subcommand arg. You may replace this with any builder you like. Builder Builder // The name of the builder, which may be the same as Subcommand, unless // it has been modified by a Matcher. Name string // The first argument passed to the Group Subcommand string // The remaining arguments passed to the Group SubcommandArgs []string }
type Matcher ¶
Matcher allows you to specify a function for resolving a builder from a list of builders when using a Group.
You could use this API to implement short aliases for existing commands too, if you so desired (i.e. "hg co" -> "hg checkout").
See GroupMatcher, GroupPrefixMatcher and PrefixMatcher.
WARNING: This API may change to return a list of possible options when the choice is ambiguous.
func PrefixMatcher ¶
PrefixMatcher returns a simple Matcher for use with a command Group that will match a command if the input is an unambiguous prefix of one of the Group's Builders.
grp := NewGroup("grp", Builders{ "foo": fooBuilder, "bar": barBuilder, "bark": barkBuilder, "bork": borkBuilder, }) // Matches must be 2 or more characters to be considered: m := PrefixMatcher(grp, 2) $ myprog grp fo // fooBuilder $ myprog grp ba // NOPE; bar or bark $ myprog grp bar // barBuilder $ myprog grp bark // barkBuilder $ myprog grp b // NOPE; too short
type QuietExit ¶
type QuietExit int
QuietExit will prevent cmdy.Fatal() from printing an error message on exit, but will still call os.Exit() with the status code it represents.
type Runner ¶
Runner builds and runs your command.
Runner provides access to standard input and output streams to cmdy.Command. Commands should access these streams via Runner rather than via os.Stdin, etc.
This is not strictly required, and some situations may necessitate using the os streams directly, but using os streams directly without a good reason limits your command's testability.
See NewBufferedRunner(), NewStandardRunner()
func DefaultRunner ¶
func DefaultRunner() *Runner
DefaultRunner is the global runner used by Run() and Fatal().
It is intended to be used once from your main() function and is not safe for concurrent use. More sophisticated use cases can be supported by creating your own Runner directly.
func NewStandardRunner ¶
func NewStandardRunner() *Runner
NewStandardRunner returns a Runner configured to use os.Stdin, os.Stdout and os.Stderr.
func (*Runner) Fatal ¶
Fatal prints an error formatted for the end user, then calls os.Exit with the exit code detected in err.
Calls to Fatal() will prevent any defer calls from running. See cmdy.Fatal() for a demonstration of the recommended usage pattern for dealing with Fatal errors.
type Usage ¶
type Usage interface {
Usage() string
}
Usage is an optional interface you can add to a Command to specify a more complete help message that will be shown by cli.Fatal() if a UsageError is returned (for example when the '-help' flag is passed).
The string returned by Usage() is parsed by the text/template package (https://golang.org/pkg/text/template/). The template makes the following functions available:
{{Invocation}} Full invocation string for the command, i.e. 'cmd sub subsub [options] <args...>'. This invocation does not include parent command flags. {{Synopsis}} Command.Synopsis() {{CommandFull}} Full command name including all parent commands, i.e. 'cmd sub subsub'. {{Command}} Current command name, not including parent command names. i.e. for command 'cmd sub subsub', only 'subsub' is returned. {{if ShowFullHelp}}...{{end}} Help section contained inside the '...' should only be shown if the command's '--help' was requested, not if the command's usage is to be shown.
If your Command does not implement cmdy.Usage, cmdy.DefaultUsage is used.
Your Command instance is used as the 'data' argument to Template.Execute(), so any exported fields from your command can be used in the template like so: "{{.MyCommandField}}".
If a Command intends cmdy to print the usage in response to an error, cmdy.UsageError or cmdy.UsageErrorf should be returned from Command.Run().
To obtain an actual usage string from a usage error, use cmdy.Format(err).
type UsageCommand ¶
UsageCommand is an aggregate interface to make it simpler for you to use Go's "implements" "keyword":
var _ cmdy.UsageCommand = &MyCommand{}