cli

package module
v0.0.0-...-f808c62 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2018 License: Apache-2.0 Imports: 14 Imported by: 6

README

turbinelabs/cli

This project is no longer maintained by Turbine Labs, which has shut down.

Apache 2.0 GoDoc CircleCI Go Report Card codecov

The cli package provides a simple library for creating command-line applications with multiple sub-commands.

Motivation

We feel strongly that all Turbine Labs executables should have high-quality and consistent ergonomics. We use the cli package for all our command line tools, private and public.

Why on earth did we choose to roll our own CLI package, with several high-quality alternatives already available (e.g. github.com/urfave/cli, github.com/spf13/cobra)? As with most libraries, it came from a combination of good intentions and hubris.

Chiefly, we wanted:

  • something fairly lightweight
  • to use the existing Go flag package
  • to support both single- and sub-command apps
  • to minimize external dependencies
  • for flags to be configurable via environment flag
  • for usage text to be both auto-generated and fairly customizable

We started with the excellent github.com/jonboulle/yacli, a "micro-pseudo-framework" which recognizes a central truth:

All CLI frameworks suck. This one just sucks slightly less.

Indeed we were attracted to its simplicity and ease of use, and used it for a time, but quickly grew weary of the attendant boilerplate necessary for each new project. We began to adapt it, to centralize code, and ended up writing a whole thing.

Now that we have it, we like it, and we welcome you to use it, or not, write your own, or contribute to ours, or whatever. It's all fine with us.

Requirements

  • Go 1.10.3 or later (previous versions may work, but we don't build or test against them)

Dependencies

The cli project depends on our nonstdlib package; The tests depend on our test package and gomock. It should always be safe to use HEAD of all master branches of Turbine Labs open source projects together, or to vendor them with the same git tag.

Additionally, we vendor github.com/mattn/go-isatty and golang.org/x/sys/unix. This should be considered an opaque implementation detail, see Vendoring for more discussion.

Install

go get -u github.com/turbinelabs/cli/...

Clone/Test

mkdir -p $GOPATH/src/turbinelabs
git clone https://github.com/turbinelabs/cli.git > $GOPATH/src/turbinelabs/cli
go test github.com/turbinelabs/cli/...

Godoc

cli

Features

The cli package includes:

  • Support for both global and per-sub-command flags
  • Automatic generation of help and version flags and (if appropriate) sub-commands
  • Basic terminal-aware formatting of usage text, including pre-formatted blocks, and bold and underlined text
  • Auto-wrapping of usage text to the terminal width
  • Support for the use of environment variables to set both global and per-sub-command flags
Environment Variables

Both global and per sub-command flags can be set by environment variable. Vars are all-caps, underscore-delimited, and replace all non-alphanumeric symbols with underscores. They are prefixed by the executable name, and by then by the subcommand if relevant.

For example, the following are equivalent:

somecmd --global-flag=a somesubcmd --cmd-flag=b
SOMECMD_GLOBAL_FLAG=a SOMECMD_SOMESUBCMD_CMD_FLAG=b somecmd

A runtime validation is available to ensure that there are no variable name collisions for a given CLI.

Help Text

Help text is generated from:

Since our target is Terminal windows, we chose to keep formatting fairly simple.

All text is passed through text/template; As such, double curly braces ("{{") in text will trigger template actions.

The cli package provides two text-styling functions for help text, for terminals that support them:

`text may rendered in {{bold "some bold text"}}`
`text may be {{ul "some underlined text}}`

To render text that contains "{{", use a pipeline action:

`here are some curlies {{ "{{something inside braces}}" }}`

Help text is also passed through go/doc, to support a few simple formatting primitives:

  • Text wraps to the current terminal width, if available, or 80 columns otherwise.
  • Whitespace, including single newlines, is collapsed in a given paragraph.
  • New paragraphs are signaled with two newlines.
  • A 4-space indent signals pre-formatted text, which is not wrapped.

Examples

Single- and multiple-command examples are available in the godoc.

Versioning

Please see Versioning of Turbine Labs Open Source Projects.

Pull Requests

Patches accepted! Please see Contributing to Turbine Labs Open Source Projects.

Code of Conduct

All Turbine Labs open-sourced projects are released with a Contributor Code of Conduct. By participating in our projects you agree to abide by its terms, which will be carefully enforced.

Documentation

Overview

The cli package provides a simple library for creating command-line applications. See http://github.com/turbinelabs/cli for a richer discussion of motivation and feature set. See the examples for basic usage.

Example (SingleCommand)

This example shows how to create a single-command CLI

// This package contains a trivial example use of the cli package
package main

import (
	"fmt"
	"strconv"

	"github.com/turbinelabs/cli"
	"github.com/turbinelabs/cli/command"
	"github.com/turbinelabs/nonstdlib/flag/usage"
)

// The typical pattern is to provide a public Cmd() func. This function should
// initialize the command.Cmd, the command.Runner, and flags.
func Cmd() *command.Cmd {
	// typically the command.Runner is initialized only with internally-defined
	// state; all necessary external state should be provided via flags. One can
	// inline the initializaton of the command.Runner in the command.Cmd
	// initialization if no flags are necessary, but it's often convenient to
	// have a typed reference
	runner := &runner{}

	cmd := &command.Cmd{
		Name:        "adder",
		Summary:     "add a delimited string of integers together",
		Usage:       "[OPTIONS] <int>...",
		Description: "add a delimited string of integers together",
		Runner:      runner,
	}

	// The flag.FlagSet is a member of the command.Cmd, and the flag
	// value is a member of the command.Runner.
	cmd.Flags.BoolVar(&runner.verbose, "verbose", false, "Produce verbose output")

	// If we wrap flag.Required(...) around the usage string, Cmd.Run(...)
	// will fail if it is unspecified
	cmd.Flags.StringVar(&runner.thing, "thing", "", usage.Required("The thing"))

	return cmd
}

// The private command.Runner implementation should contain any state needed
// to execute the command. The values should be initialized via flags declared
// in the Cmd() function.
type runner struct {
	verbose bool
	thing   string
}

// Run does the actual work, based on state provided by flags, and the
// args remaining after the flags have been parsed.
func (f *runner) Run(cmd *command.Cmd, args []string) command.CmdErr {
	ints := []int{}
	sum := 0
	// argument validation should occur at the top of the function, and
	// errors should be reported via the cmd.BadInput or cmd.BadInputf methods.
	// In this case, the main work of the function is done at the same time.
	for _, arg := range args {
		i, err := strconv.Atoi(arg)
		if err != nil {
			return cmd.BadInputf("Bad integer: \"%s\": %s", arg, err)
		}
		ints = append(ints, i)
		sum += i
	}

	if f.verbose && len(ints) > 0 {
		fmt.Print(ints[0])
		for _, i := range ints[1:] {
			fmt.Print(" + ", i)
		}
		fmt.Print(" = ")
	}

	fmt.Printf(`The thing: %s, the sum: %d`, f.thing, sum)

	// In this case, there was no error. Errors should be returned via the
	// cmd.Error or cmd.Errorf methods.
	return command.NoError()
}

func mkSingleCmdCLI() cli.CLI {
	// make a new CLI passing the version string and a command.Cmd
	// while it's possible to add flags to the CLI, they are ignored; only the
	// Cmd's flags are presented to the user.
	return cli.New("1.0.2", Cmd())
}

// This example shows how to create a single-command CLI
func main() {
	// this would be your main() function

	// run the Main function, which calls os.Exit with the appropriate exit status
	mkSingleCmdCLI().Main()
}

// Add the following to your tests to validate that there are no collisions
// between command flags and that help text can be generated without error:

// package main

// import (
// 	"testing"

// 	"github.com/turbinelabs/test/assert"
// )

// func TestCLI(t *testing.T) {
// 	assert.Nil(t, mkCLI().Validate())
// }
Output:

Example (SubCommands)

This example shows how to create a CLI with multiple sub-commands

// This package contains a trivial example use of the cli package
package main

import (
	"fmt"
	"strings"

	"github.com/turbinelabs/cli"
	"github.com/turbinelabs/cli/command"
)

// The typical pattern is to provide a public CmdXYZ() func for each
// sub-command you wish to provide. This function should initialize the
// command.Cmd, the command.Runner, and flags.
func CmdSplit() *command.Cmd {
	// typically the command.Runner is initialized only with internally-defined
	// state; all necessary external state should be provided via flags. One can
	// inline the initializaton of the command.Runner in the command.Cmd
	// initialization if no flags are necessary, but it's often convenient to
	// have a typed reference
	runner := &splitRunner{}

	cmd := &command.Cmd{
		Name:        "split",
		Summary:     "split strings",
		Usage:       "[OPTIONS] <string>",
		Description: "split strings using the specified delimiter",
		Runner:      runner,
	}

	// The flag.FlagSet is a member of the command.Cmd, and the flag
	// value is a member of the command.Runner.
	cmd.Flags.StringVar(&runner.delim, "delim", ",", "The delimiter on which to split the string")

	return cmd
}

// The private command.Runner implementation should contain any state needed
// to execute the command. The values should be initialized via flags declared
// in the CmdXYZ() function.
type splitRunner struct {
	delim string
}

// Run does the actual work, based on state provided by flags, and the
// args remaining after the flags have been parsed.
func (f *splitRunner) Run(cmd *command.Cmd, args []string) command.CmdErr {
	// argument validation should occur at the top of the function, and
	// errors should be reported via the cmd.BadInput or cmd.BadInputf methods
	if len(args) < 1 {
		return cmd.BadInput("missing \"string\" argument.")
	}
	str := args[0]
	if globalFlags.verbose {
		fmt.Printf("Splitting \"%s\"\n", str)
	}
	split := strings.Split(str, f.delim)
	for i, term := range split {
		if globalFlags.verbose {
			fmt.Printf("[%d] ", i)
		}
		fmt.Println(term)
	}

	// In this case, there was no error. Errors should be returned via the
	// cmd.Error or cmd.Errorf methods.
	return command.NoError()
}

// A second command
func CmdJoin() *command.Cmd {
	runner := &joinRunner{}

	cmd := &command.Cmd{
		Name:        "join",
		Summary:     "join strings",
		Usage:       "[OPTIONS] <string>...",
		Description: "join strings using the specified delimiter",
		Runner:      runner,
	}

	cmd.Flags.StringVar(&runner.delim, "delim", ",", "The delimiter with which to join the strings")

	return cmd
}

// a second Runner
type joinRunner struct {
	delim string
}

func (f *joinRunner) Run(cmd *command.Cmd, args []string) command.CmdErr {
	if globalFlags.verbose {
		fmt.Printf("Joining \"%v\"\n", args)
	}
	joined := strings.Join(args, f.delim)
	fmt.Println(joined)

	return command.NoError()
}

// while not manditory, keeping globally-configured flags in a single struct
// makes it obvious where they came from at access time.
type globalFlagsT struct {
	verbose bool
}

var globalFlags = globalFlagsT{}

func mkSubCmdCLI() cli.CLI {
	// make a new CLI passing the description and version and one or more sub commands
	c := cli.NewWithSubCmds(
		"an example CLI for simple string operations",
		"1.2.3",
		CmdSplit(),
		CmdJoin(),
	)

	// Global flags can be used to modify global state
	c.Flags().BoolVar(&globalFlags.verbose, "verbose", false, "Produce verbose output")

	return c
}

// This example shows how to create a CLI with multiple sub-commands
func main() {
	// this would be your main() function

	// run the Main function, which calls os.Exit with the appropriate exit status
	mkSubCmdCLI().Main()
}

// Add the following to your tests to validate that there are no collisions
// between command flags:

// package main

// import (
// 	"testing"

// 	"github.com/turbinelabs/test/assert"
// )

// func TestCLI(t *testing.T) {
// 	assert.Nil(t, mkCLI().Validate())
// }
Output:

Index

Examples

Constants

View Source
const HelpSummary = "Show a list of commands or help for one command"
View Source
const VersionSummary = "Print the version and exit"

Variables

This section is empty.

Functions

This section is empty.

Types

type CLI

type CLI interface {
	// Flags returns a pointer to the global flags for the CLI
	Flags() *flag.FlagSet
	// Set the flags
	SetFlags(*flag.FlagSet)

	// Main serves as the main() function for the CLI. It will parse
	// the command-line arguments and flags, call the appropriate sub-command,
	// and return exit status and output error messages as appropriate.
	Main()

	// Validate can be used to make sure the CLI is well-defined from within
	// unit tests. In particular it will validate that no two flags exist with
	// the same environment key. As a last-ditch effort, Validate will be called
	// at the start of Main. ValidationFlag values may be passed to alter the
	// level of validation performed.
	Validate(...ValidationFlag) error

	// Returns the CLI version data.
	Version() app.Version
}

A CLI represents a command-line application

func New

func New(version string, command *command.Cmd) CLI

New produces a CLI for the given command.Cmd

func NewWithSubCmds

func NewWithSubCmds(
	description string,
	version string,
	command1 *command.Cmd,
	commandsN ...*command.Cmd,
) CLI

NewWithSubCmds produces a CLI for the given app.App and with subcommands for the given command.Cmds.

type ValidationFlag

type ValidationFlag int
const (
	// Skips Validating that global and subcommand help text can
	// be generated.
	ValidateSkipHelpText ValidationFlag = iota
)

Directories

Path Synopsis
The app package provides a simple App struct to describe a command-line application, and a Usage interface, which describers the global and command-specific usage of the App.
The app package provides a simple App struct to describe a command-line application, and a Usage interface, which describers the global and command-specific usage of the App.
The command package provides an abstraction for a command-line application sub-command, a means to execute code when that sub-command is invoked, a means to report success/failure status of said code, and generic implementations of help and version sub-commands.
The command package provides an abstraction for a command-line application sub-command, a means to execute code when that sub-command is invoked, a means to report success/failure status of said code, and generic implementations of help and version sub-commands.

Jump to

Keyboard shortcuts

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