cmdy

package module
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 14, 2020 License: MIT Imports: 12 Imported by: 0

README

cmdy: Go library for implementing CLI programs

GoDoc

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 is liberally documented in GoDoc, though a brief overview is provided in this README. If anything is unclear, please submit a GitHub issue.

Features

  • ArgSet, similar to flag.FlagSet but for positional arguments. The github.com/shabbyrobe/cmdy/arg package can be used independently.
  • Simple subcommand (and sub-sub command (and sub-sub-sub command)) support.
  • context.Context support (via cmdy.Context, which is also a context.Context).
  • Automatic (but customisable) usage and invocation strings.

Usage

Subcommands are easy to create; you need a builder and one or more implementations of cmdy.Command. This fairly contrived example demonstrates the basics:

type demoCommand struct {
	testFlag string
	testArg  string
	rem      []string
}

func newDemoCommand() cmdy.Command {
	return &demoCommand{}
}

func (t *demoCommand) Synopsis() string { return "My command is a command that does stuff" }

func (t *demoCommand) Configure(flags *cmdy.FlagSet, args *arg.ArgSet) {
	flags.StringVar(&t.testFlag, "test", "", "Test flag")
	args.String(&t.testArg, "test", "Test arg")
	args.Remaining(&t.rem, "things", arg.AnyLen, "Any number of extra string arguments.")
}

func (t *demoCommand) Run(ctx cmdy.Context) error {
	fmt.Println(t.testFlag, t.testArg, t.rem)
	return nil
}

func main() {
	if err := run(); err != nil {
		cmdy.Fatal(err)
	}
}

func run() error {
	nestedGroupBuilder := func() cmdy.Command {
		return cmdy.NewGroup(
			"Nested group",
			cmdy.Builders{"subcmd": newDemoCommand},
		)
	}

	mainGroupBuilder := func() cmdy.Command {
		return cmdy.NewGroup(
			"My command group",
			cmdy.Builders{
				"cmd":  newDemoCommand,
				"nest": nestedGroupBuilder,
			},
		)
	}
	return cmdy.Run(context.Background(), os.Args[1:], mainGroupBuilder)
}

You can customise the help message by implementing the optional cmdy.Usage interface. Usage() is a go text/template that has access to your command as its vars:

const myCommandUsage = `
{{Synopsis}}

Usage: {{Invocation}}

Stuff is {{.Stuff}}
`

type myCommand struct {
    Stuff string
}

var _ cmdy.Usage = &myCommand{}

func (t *myCommand) Usage() string { return myCommandUsage }

// myCommand implements the rest of cmdy.Command...

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

Examples

Constants

View Source
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

View Source
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

View Source
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 CommandPath(ctx Context) (out []string)

func ErrWithCode

func ErrWithCode(code int, err error) error

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 FormatError(err error) (msg string, code int)

func HelpRequest

func HelpRequest() error

HelpRequest wraps an existing error so that cmdy.Fatal() will print the full command help.

func IsUsageError

func IsUsageError(err error) bool

func ProgName

func ProgName() string

ProgName attempts to guess the program name from the first argument in os.Args.

func ReaderIsPipe

func ReaderIsPipe(in io.Reader) bool

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 Reset

func Reset()

Reset is here just for testing purposes.

func Run

func Run(ctx context.Context, args []string, b Builder) (rerr error)

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

func UsageError(err error) error

UsageError wraps an existing error so that cmdy.Fatal() will print the full command usage above the error message.

func UsageErrorf

func UsageErrorf(msg string, args ...interface{}) error

UsageErrorf formats an error message so that cmdy.Fatal() will print the full command usage above it.

func WriterIsPipe

func WriterIsPipe(out io.Writer) bool

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 Builders

type Builders map[string]Builder

type Command

type Command interface {
	// Synopsis is the shortest possible complete description of your command,
	// ideally one sentence.
	Synopsis() string

	Configure(flags *FlagSet, args *arg.ArgSet)

	Run(Context) error
}

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

type CommandRef struct {
	Name    string
	Command Command
}

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 Error

type Error interface {
	Code() int
	error
}

type FlagSet

type FlagSet struct {
	*flag.FlagSet
	WrapWidth int
	// contains filtered or unexported fields
}

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

func FlagSetFromStd(fs *flag.FlagSet) *FlagSet

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

func (fs *FlagSet) Invocation() string

Invocation string for the flags, for example '[-foo=<yep>] [-bar=<pants>]`. If there are too many flags, `[options]` is returned instead.

func (*FlagSet) Usage

func (fs *FlagSet) Usage() string

Usage returns the full usage string for the FlagSet, provided HideUsage() has not been set.

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

func NewGroup

func NewGroup(synopsis string, builders Builders, opts ...GroupOption) *Group

func (*Group) Builder

func (cs *Group) Builder(cmd string) (bld Builder, name string, rerr error)

func (*Group) Configure

func (cs *Group) Configure(flags *FlagSet, args *arg.ArgSet)

func (*Group) Flags

func (cs *Group) Flags() *FlagSet

func (*Group) Run

func (cs *Group) Run(ctx Context) error

func (*Group) Synopsis

func (cs *Group) Synopsis() string

func (*Group) Usage

func (cs *Group) Usage() string

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

type Matcher func(bldrs Builders, in string) (bld Builder, name string, rerr error)

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

func PrefixMatcher(group *Group, minLen int) Matcher

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.

func (QuietExit) Code

func (e QuietExit) Code() int

func (QuietExit) Error

func (e QuietExit) Error() string

type Runner

type Runner struct {
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer
}

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

func (r *Runner) Fatal(err error)

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.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, name string, args []string, b Builder) (rerr error)

Run builds and runs your command.

If using Run() in your main() function, the returned error should be passed to Runner.Fatal(), not log.Fatal(), if you want nice errors and usage printed.

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

type UsageCommand interface {
	Command
	Usage
}

UsageCommand is an aggregate interface to make it simpler for you to use Go's "implements" "keyword":

var _ cmdy.UsageCommand = &MyCommand{}

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL