command

package module
v0.1.13 Latest Latest
Warning

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

Go to latest
Published: Mar 18, 2024 License: BSD-3-Clause Imports: 16 Imported by: 16

README

command

GoDoc

This repository provides Go package that implements basic subcommand dispatch for Go command-line programs. The design of this package is based on the way the "go" command-line tool works, but the implementation is not shared.

This package is in development, the API is likely to change in various ways.

Documentation

Overview

Package command defines plumbing for command dispatch. It is based on and similar in design to the "go" command-line tool.

Overview

The command package helps a program to process a language of named commands, each of which may have its own flags, arguments, and nested subcommands. A command is represented by a *command.C value carrying help text, usage summaries, and a function to execute its behavior.

The Run and RunOrFail functions parse the raw argument list of a program against a tree of *command.C values, parsing flags as needed and executing the selected command or printing appropriate diagnostics. Flags are parsed using the standard "flag" package by default.

Example
package main

import (
	"flag"
	"fmt"
	"io"
	"strings"

	"github.com/creachadair/command"
)

func main() {
	// The environment passed to a command can carry an arbitrary config value.
	// Here we use a struct carrying information about options.
	type options struct {
		noNewline bool
		label     string
		private   int
		confirm   bool
	}

	root := &command.C{
		Name: "example",

		// Usage may have multiple lines, and can omit the command name.
		Usage: "command args...",

		// The first line of the help text is used as "short" help.
		// Any subsequent lines are include in "long" help.
		Help: `Do interesting things with arguments.

This program demonstrates the use of the command package.
This help text is printed by the "help" subcommand.`,

		// This hook is called (when defined) to set up flags.
		SetFlags: func(env *command.Env, fs *flag.FlagSet) {
			opt := env.Config.(*options)
			fs.StringVar(&opt.label, "label", "", "Label text")
			fs.IntVar(&opt.private, "p", 0, "PRIVATE: Unadvertised flag")
			fs.BoolVar(&opt.confirm, "y", false, "Confirm activity")
			env.MergeFlags(true)
		},

		// Note that the "example" command does not have a Run function.
		// Executing it without a subcommand will print a help message and exit
		// with error.

		Commands: []*command.C{
			// This function creates a basic "help" command that prints out
			// command help based on usage and help strings.
			//
			// This command can also have "help topics", which are stripped-down
			// commands with only help text (no flags, usage, or other behavior).
			command.HelpCommand([]command.HelpTopic{{
				Name: "special",
				Help: "This is some useful information a user might care about.",
			}, {
				Name: "magic",
				Help: `The user can write "command help <topic>" to get this text.`,
			}}),

			// This is a typical user-defined command.
			{
				Name:  "echo",
				Usage: "text ...",
				Help:  "Concatenate the arguments with spaces and print to stdout.",

				SetFlags: func(env *command.Env, fs *flag.FlagSet) {
					// Pull the config value out of the environment and attach a flag to it.
					opt := env.Config.(*options)
					fs.BoolVar(&opt.noNewline, "n", false, "Do not print a trailing newline")
				},

				Run: func(env *command.Env) error {
					opt := env.Config.(*options)
					if opt.label != "" {
						fmt.Printf("[%s] ", opt.label)
					}
					if opt.private > 0 {
						fmt.Printf("<%d> ", opt.private)
					}
					fmt.Print(strings.Join(env.Args, " "))
					if !opt.noNewline {
						fmt.Println()
					}
					return nil
				},
			},

			{
				Name:  "secret",
				Usage: "args ...",
				Help:  "A command that is hidden from help listings.",

				// Exclude this command when listing subcommands.
				Unlisted: true,

				Run: func(env *command.Env) error {
					fmt.Printf("easter-egg %s\n", strings.Join(env.Args, ", "))
					return nil
				},
			},
		},
	}

	// Demonstrate help output.
	//
	// Note that the argument to NewEnv is plumbed via the Config field of Env.
	var opt options
	env := root.NewEnv(&opt)
	env.Log = io.Discard

	command.Run(env, []string{"help"})
	opt = options{} // reset settings

	// Requesting help for an unlisted subcommand reports an error.
	command.Run(env, []string{"help", "secret"})
	opt = options{}

	// But if you name the command explicitly with -help, you get help.
	command.Run(env, []string{"secret", "-help"})
	opt = options{}

	// Execute a command with some arguments.
	command.RunOrFail(env, []string{"echo", "foo", "bar"})
	opt = options{}

	// Execute a command with some flags and argujments.
	command.RunOrFail(env, []string{"-label", "xyzzy", "echo", "bar"})
	opt = options{}

	// Merged flags can be used anywhere in their scope.
	command.RunOrFail(env, []string{"echo", "-label", "foo", "bar"})
	opt = options{}

	// Private-marked flags still work as expected.
	command.RunOrFail(env, []string{"echo", "-p", "25", "-label", "ok", "bar"})
	opt = options{}

	// Executing an unlisted command works.
	command.RunOrFail(env, []string{"secret", "fort"})
	opt = options{}

	// An unmerged flag.
	command.RunOrFail(env, []string{"echo", "-n", "baz"})
}
Output:

foo bar
[xyzzy] bar
[foo] bar
[ok] <25> bar
easter-egg fort
baz

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrRequestHelp = errors.New("help requested")

ErrRequestHelp is returned from Run if the user requested help.

Functions

func Adapt added in v0.0.5

func Adapt(fn any) func(*Env) error

Adapt adapts a more general function to the type signature of a Run function. The value of fn must be a function with a type signature like:

func(*command.Env) error
func(*command.Env, s1, s2 string) error
func(*command.Env, s1, s2 string, more ...string) error
func(*command.Env, s1, s2 string, rest []string) error

That is, its first argument must be a *command.Env, it must return an error, and the rest of its arguments must be strings except the last, which may be a slice of strings (a "rest parameter").

The adapted function checks that the arguments presented match the number of strings accepted by fn. If fn is variadic or has a rest parameter, at least as many arguments must be provided as the number of fixed parameters. Otherwise, the number of arguments must match exactly. If this fails, the adapted function reports an error without calling fn. Otherwise, the adapter calls fn and returns its result.

Adapt will panic if fn is not a function of a supported type.

func FailWithUsage

func FailWithUsage(env *Env) error

FailWithUsage is a run function that logs a usage message for the command and returns ErrRequestHelp.

func Flags added in v0.0.6

func Flags(bind func(*flag.FlagSet, any), vs ...any) func(*Env, *flag.FlagSet)

Flags returns a SetFlags function that calls bind(fs, v) for each v and the given flag set.

func ProgramName added in v0.1.11

func ProgramName() string

ProgramName returns the base name of the currently-running executable.

func Run

func Run(env *Env, rawArgs []string) (err error)

Run traverses the given unprocessed arguments starting from env. See the documentation for type C for a description of argument traversal.

Run writes usage information to env and returns a UsageError if the command-line usage was incorrect, or ErrRequestHelp if the user requested help via the --help flag.

func RunHelp

func RunHelp(env *Env) error

RunHelp is a run function that implements long help. It displays the help for the enclosing command or subtopics of "help" itself.

func RunOrFail

func RunOrFail(env *Env, rawArgs []string)

RunOrFail behaves as Run, but prints a log message and calls os.Exit if the command reports an error. If the command succeeds, RunOrFail returns.

If a command reports a UsageError or ErrRequestHelp, the exit code is 2. For any other error the exit code is 1.

Types

type C

type C struct {
	// The name of the command, preferably one word. The name is used during
	// argument processing to choose which command or subcommand to execute.
	Name string

	// A terse usage summary for the command. Multiple lines are allowed.
	// Each line should be self-contained for a particular usage sense.
	//
	// The name of the command will be automatically inserted at the front of
	// each usage line if it is not present. If no usage is defined, the help
	// mechanism will generate a default based on the presence of flags and
	// subcommands.
	Usage string

	// A detailed description of the command. Multiple lines are allowed.
	// The first non-blank line of this text is used as a synopsis; the whole
	// string is printed for long help.
	Help string

	// Flags parsed from the raw argument list. This will be initialized before
	// Init or Run is called.
	Flags flag.FlagSet

	// If false, Flags is used to parse the argument list.  Otherwise, the Init
	// function is responsible for parsing flags from the argument list.
	CustomFlags bool

	// If true, exclude this command from help listings unless it is explicitly
	// named and requested.
	Unlisted bool

	// Perform the action of the command. If nil, calls FailWithUsage.
	Run func(env *Env) error

	// If set, this will be called before flags are parsed, to give the command
	// an opportunity to set flags.
	SetFlags func(env *Env, fs *flag.FlagSet)

	// If set, this will be called after flags are parsed (if any) but before
	// any subcommands are processed. If it reports an error, execution stops
	// and that error is returned to the caller.
	//
	// The Init callback is permitted to modify env, and any such modifications
	// will persist through the rest of the invocation.
	Init func(env *Env) error

	// Subcommands of this command.
	Commands []*C
	// contains filtered or unexported fields
}

C carries the description and invocation function for a command.

To process a command-line, the Run function walks through the arguments starting from a root command to discover which command should be run and what flags it requires. This argument traversal proceeds in phases:

When a command is first discovered during argument traversal, its SetFlags hook is executed (if defined) to prepare its flag set. Then, unless the CustomFlags option is true, the rest of the argument list is parsed using the FlagSet to separate command-specific flags from further arguments and/or subcommands.

After flags are parsed and before attempting to explore subcommands, the current command's Init hook is called (if defined). If Init reports an error it terminates argument traversal, and that error is reported back to the user.

Next, if there are any remaining non-flag arguments, Run checks whether the current command has a subcommand matching the first argument. If so argument traversal recurs into that subcommand to process the rest of the command-line.

Otherwise, if the command defines a Run hook, that hook is executed with the remaining unconsumed arguments. If no Run hook is defined, the traversal stops, logs a help message, and reports an error.

func HelpCommand

func HelpCommand(topics []HelpTopic) *C

HelpCommand constructs a standardized help command with optional topics. The caller is free to edit the resulting command, each call returns a separate value.

As a special case, if there are arguments after the help command and the first is one of "-a", "-all", or "--all", that argument is discarded and the rendered help text includes unlisted commands and private flags.

func VersionCommand

func VersionCommand() *C

VersionCommand constructs a standardized version command that prints version metadata from the running binary to stdout. The caller can safely modify the returned command to customize its behavior.

func (*C) FindSubcommand

func (c *C) FindSubcommand(name string) *C

FindSubcommand returns the subcommand of c matching name, or nil.

func (*C) HasRunnableSubcommands

func (c *C) HasRunnableSubcommands() bool

HasRunnableSubcommands reports whether c has any runnable subcommands.

func (*C) HelpInfo

func (c *C) HelpInfo(flags HelpFlags) HelpInfo

HelpInfo returns help details for c.

A command or subcommand with no Run function and no subcommands of its own is considered a help topic, and listed separately.

Flags whose usage message has the case-sensitive prefix "PRIVATE:" are omitted from help listings.

func (*C) NewEnv

func (c *C) NewEnv(config any) *Env

NewEnv returns a new root context for c with the optional config value.

func (*C) Runnable

func (c *C) Runnable() bool

Runnable reports whether the command has any action defined.

type Env

type Env struct {
	Parent  *Env      // if this is a subcommand, its parent environment (or nil)
	Command *C        // the C value that carries the Run function
	Config  any       // configuration data
	Args    []string  // the unclaimed command-line arguments
	Log     io.Writer // where to write diagnostic output (nil for os.Stderr)
	// contains filtered or unexported fields
}

Env is the environment passed to the Run function of a command. An Env implements the io.Writer interface, and should be used as the target of any diagnostic output the command wishes to emit. Primary command output should be sent to stdout.

func (*Env) Cancel added in v0.0.2

func (e *Env) Cancel(cause error)

Cancel cancels the context associated with e with the given cause. If e does not have its own context, the cancellation is propagated to its parent if one exists. If e has no parent and no context, Cancel does nothing without error.

func (*Env) Context added in v0.0.2

func (e *Env) Context() context.Context

Context returns the context associated with e. If e does not have its own context, it returns the context of its parent, or if e has no parent it returns a new background context.

func (*Env) HelpFlags added in v0.1.3

func (e *Env) HelpFlags(f HelpFlags) *Env

HelpFlags sets the base help flags for e and returns e.

By default, help listings do not include unlisted commands or private flags. This permits the caller to override the default help printing rules.

func (*Env) MergeFlags added in v0.1.0

func (e *Env) MergeFlags(merge bool) *Env

MergeFlags sets the flag merge option for e and returns e.

Setting this option true modifies the flag parsing algorithm for commands dispatched through e to "merge" flags matching the current command from the remainder of the argument list. The default is false.

Merging allows flags for a command to be defined later in the command-line, after subcommands and their own flags. For example, given a command "one" with flag -A and a subcommand "two" with flag -B: With merging false, the following arguments report an error.

one two -B 2 -A 1

This is because the default parsing algorithm (without merge) stops parsing flags for "one" at the first non-flag argument, and "two" does not recognize the flag -A. With merging enabled the argument list succeeds, because the parser "looks ahead", treating it as if the caller had written:

one -A 1 two -B 2

Setting the MergeFlags option also applies to all the descendants of e unless the command's Init callback changes the setting. Note that if a subcommand defines a flag with the same name as its ancestor, the ancestor will shadow the flag for the descendant.

func (*Env) SetContext added in v0.0.2

func (e *Env) SetContext(ctx context.Context) *Env

SetContext sets the context of e to ctx and returns e. If ctx == nil it clears the context of e so that it defaults to its parent (see Context).

func (*Env) Usagef

func (e *Env) Usagef(msg string, args ...any) error

Usagef returns a formatted error that describes a usage error for the command whose environment is e. The result has concrete type UsageError.

func (*Env) Write

func (e *Env) Write(data []byte) (int, error)

Write implements the io.Writer interface. Writing to a context writes to its designated output stream, allowing the context to be sent diagnostic output.

type HelpFlags added in v0.1.0

type HelpFlags int

HelpFlags is a bit mask of flags for the HelpInfo method.

const (
	IncludeCommands     HelpFlags = 1 << iota // include subcommands and help topics
	IncludeUnlisted                           // include unlisted subcommands
	IncludePrivateFlags                       // include private (hidden) flags
)

type HelpInfo

type HelpInfo struct {
	Name     string
	Synopsis string
	Usage    string
	Help     string
	Flags    string

	// Help for subcommands (populated if requested)
	Commands []HelpInfo

	// Help for subtopics (populated if requested)
	Topics []HelpInfo
}

HelpInfo records synthesized help details for a command.

func (HelpInfo) WriteLong

func (h HelpInfo) WriteLong(w io.Writer)

WriteLong writes a complete help description to w, including a usage summary, full help text, flag summary, and subcommands.

func (HelpInfo) WriteSynopsis

func (h HelpInfo) WriteSynopsis(w io.Writer)

WriteSynopsis writes a usage summary and command synopsis to w. If the command defines flags, the flag summary is also written.

func (HelpInfo) WriteUsage

func (h HelpInfo) WriteUsage(w io.Writer)

WriteUsage writes a usage summary to w.

type HelpTopic

type HelpTopic struct {
	Name string
	Help string
}

A HelpTopic specifies a name and some help text for use in constructing help topic commands.

type UsageError

type UsageError struct {
	Env     *Env
	Message string
}

UsageError is the concrete type of errors reported by the Usagef function, indicating an error in the usage of a command.

func (UsageError) Error

func (u UsageError) Error() string

type VersionInfo added in v0.1.7

type VersionInfo struct {
	// Name is the base name of the running binary from os.Args.
	Name string `json:"name"`

	// BinaryPath is the path of the program binary as reported by os.Executable.
	BinaryPath string `json:"binaryPath,omitempty"`

	// ImportPath is the Go import path of the main package.
	ImportPath string `json:"importPath"`

	// Version, if available, is the version tag at which the binary was built.
	// This is empty if no version label is available, e.g. an untagged commit.
	Version string `json:"version,omitempty"`

	// Commit, if available, is the commit hash at which the binary was built.
	// Typically this will be a hex string, but the format is not guaranteed.
	Commit string `json:"commit,omitempty"`

	// Modified reports whether the contents of the build environment were
	// modified from a clean state. This may indicate the presence of extra
	// files in the working directory, even if the repository is up-to-date.
	Modified bool `json:"modified,omitempty"`

	// Toolchain gives the Go toolchain version that built the binary.
	Toolchain string `json:"toolchain"`

	// OS gives the GOOS value used by the compiler.
	OS string `json:"os,omitempty"`

	// Arch gives the GOARCH value used by the compiler.
	Arch string `json:"arch,omitempty"`

	// Time, if non-nil, gives the timestamp corresponding to the commit at
	// which the binary was built.  It is nil if the commit time is not
	// recorded; otherwise the value is a non-zero time in UTC.
	Time *time.Time `json:"time,omitempty"`
}

VersionInfo records version information extracted from the build info record for the running program.

func GetVersionInfo added in v0.1.7

func GetVersionInfo() VersionInfo

GetVersionInfo returns a VersionInfo record extracted from the build metadata in the currently running process. If no build information is available, only the Name field will be populated.

func (VersionInfo) String added in v0.1.7

func (v VersionInfo) String() string

String encodes v in a single-line human-readable format. This is the format used for plain text output by the "version" command implementation.

Jump to

Keyboard shortcuts

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