subcmd

package module
v2.2.2 Latest Latest
Warning

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

Go to latest
Published: Nov 27, 2023 License: MIT Imports: 13 Imported by: 4

README

Subcmd - command-line interfaces with subcommands and flags

Go Reference Go Report Card Tests Coverage Status

This is subcmd, a Go package for writing command-line programs that require flag parsing and that have “subcommands” that also require flag parsing.

Use it when you want your program to parse command lines that look like this:

command -globalopt subcommand -subopt1 FOO -subopt2 ARG1 ARG2

Subcommands may have sub-subcommands and so on. Subcommands may also be implemented as separate executables.

This is a layer on top of the standard Go flag package.

Usage

import (
  "context"
  "database/sql"
  "flag"

  "github.com/bobg/subcmd/v2"
)

func main() {
  // Parse global flags normally.
  dbname := flag.String("db", "", "database connection string")
  flag.Parse()

  db, err := sql.Open(dbdriver, *dbname)
  if err != nil { ... }

  // Stash global options in a top-level command object.
  c := command{db: db}

  // Run the subcommand given in the remainder of the command line.
  err = subcmd.Run(context.Background(), c, flag.Args())
  if err != nil { ... }
}

// The top-level command object.
type command struct {
  db *sql.DB
}

// To be used in subcmd.Run above, `command` must implement this method.
func (c command) Subcmds() subcmd.Map {
  return subcmd.Commands(
    // The "list" subcommand takes one flag, -reverse.
    "list", c.list, "list employees", subcmd.Params(
      "-reverse", subcmd.Bool, false, "reverse order of list",
    ),

    // The "add" subcommand takes no flags but one positional argument.
    "add", c.add, "add new employee", subcmd.Params(
      "name", subcmd.String, "", "employee name",
    )
  )
}

// Implementation of the "list" subcommand.
// The value of the -reverse flag is passed as an argument.
func (c command) list(ctx context.Context, reverse bool, _ []string) error {
  query := "SELECT name FROM employees ORDER BY name"
  if reverse {
    query += " DESC"
  }
  rows, err := c.db.QueryContext(ctx, query)
  if err != nil { ... }
  defer rows.Close()
  for rows.Next() { ... }
  return rows.Err()
}

// Implementation of the "add" subcommand.
func (c command) add(ctx context.Context, name string, _ []string) error {
  _, err := c.db.ExecContext(ctx, "INSERT INTO employees (name) VALUES ($1)", name)
  return err
}

Documentation

Overview

Package subcmd provides types and functions for creating command-line interfaces with subcommands and flags.

Example
package main

import (
	"context"
	"database/sql"
	"flag"

	"github.com/bobg/subcmd/v2"
)

func main() {
	// First, parse global flags normally.
	var (
		driver = flag.String("driver", "sqlite3", "database driver")
		dbname = flag.String("db", "", "database name")
	)
	flag.Parse()

	db, err := sql.Open(*driver, *dbname)
	if err != nil {
		panic(err)
	}

	// Stash the relevant info into an object that implements the subcmd.Cmd interface.
	cmd := command{db: db}

	// Pass that object to subcmd.Run,
	// which will parse a subcommand and its flags
	// from the remaining command-line arguments
	// and run them.
	err = subcmd.Run(context.Background(), cmd, flag.Args())
	if err != nil {
		panic(err)
	}
}

// Type command implements the subcmd.Cmd interface,
// meaning that it can report its subcommands,
// their names,
// and their parameters and types
// via the Subcmds method.
type command struct {
	db *sql.DB
}

func (c command) Subcmds() subcmd.Map {
	return subcmd.Commands(
		// The "list" subcommand takes one flag, -reverse.
		"list", c.list, "list employees", subcmd.Params(
			"-reverse", subcmd.Bool, false, "reverse order of list",
		),

		// The "add" subcommand takes no flags but one positional argument.
		"add", c.add, "add new employee", subcmd.Params(
			"name", subcmd.String, "", "employee name",
		),
	)
}

// The implementation of a subcommand takes a context object,
// the values of its parsed flags and positional arguments,
// and a slice of remaining command-line arguments
// (which could be used in another call to subcmd.Run
// to implement a sub-subcommand).
// It can optionally return an error.
func (c command) list(ctx context.Context, reverse bool, _ []string) error {
	query := "SELECT name FROM employees ORDER BY name"
	if reverse {
		query += " DESC"
	}
	rows, err := c.db.QueryContext(ctx, query)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		// ...print the employee name contained in this row...
	}
	return rows.Err()
}

// Implementation of the "add" subcommand.
func (c command) add(ctx context.Context, name string, _ []string) error {
	_, err := c.db.ExecContext(ctx, "INSERT INTO employees (name) VALUES ($1)", name)
	return err
}
Output:

Index

Examples

Constants

View Source
const EnvVar = "SUBCMD_ENV"

EnvVar is the name of the environment variable used by Run to pass the JSON-encoded Cmd to a subprocess. Use ParseEnv to decode it. See Prefixer.

Variables

View Source
var ErrTooFewArgs = errors.New("too few arguments")

ErrTooFewArgs is the error when not enough arguments are supplied for required positional parameters.

Functions

func Check added in v2.1.0

func Check(subcmd Subcmd) error

Check checks that the type of subcmd.F matches the expectations set by subcmd.Params:

  • It must be a function;
  • It must return no more than one value;
  • If it returns a value, that value must be of type error;
  • It must take an initial context.Context parameter;
  • It must take a final []string or ...string parameter;
  • The length of subcmd.Params must match the number of parameters subcmd.F takes (not counting the initial context.Context and final []string parameters);
  • Each parameter in subcmd.Params must match the corresponding parameter in subcmd.F.

It also checks that the default value of each parameter in subcmd.Params matches the parameter's type.

func CheckMap added in v2.1.0

func CheckMap(m Map) error

CheckMap calls Check on each of the entries in the Map.

func FlagSet

func FlagSet(ctx context.Context) *flag.FlagSet

FlagSet produces the flag.FlagSet used in a call to a Subcmd function.

func ParseEnv added in v2.1.0

func ParseEnv(ptr interface{}) error

ParseEnv parses the value of the SUBCMD_ENV environment variable, placing the result in the value pointed to by ptr, which must be a pointer of a suitable type. Executables that implement subcommands should run this at startup.

func Run

func Run(ctx context.Context, c Cmd, args []string) error

Run runs the subcommand of c named in args[0].

That subcommand specifies zero or more flags and zero or more positional parameters. The remaining values in args are parsed to populate those.

The subcommand's function is invoked with the given context object, the parsed flag and positional-parameter values, and a slice of the values remaining in args after parsing.

Flags are parsed using a new flag.FlagSet, which is placed into the context object passed to the subcommand's function. The FlagSet can be retrieved if needed with the FlagSet function. No flag.FlagSet is present if the subcommand has no flags.

Flags are always optional, and have names beginning with "-". Positional parameters may be required or optional. Optional positional parameters have a trailing "?" in their names.

Calling Run with an empty args slice produces a MissingSubcmdErr error.

Calling Run with an unknown subcommand name in args[0] produces an UnknownSubcmdErr error, unless the unknown subcommand is "help", in which case the result is a HelpRequestedErr, or unless c is also a Prefixer.

If c is a Prefixer and the subcommand name is both unknown and not "help", then an executable is sought in $PATH with c's prefix plus the subcommand name. (For example, if c.Prefix() returns "foo-" and the subcommand name is "bar", then the executable "foo-bar" is sought.) If one is found, it is executed with the remaining args as arguments, and a JSON-marshaled copy of c in the environment variable SUBCMD_ENV (that can be parsed by the subprocess using ParseEnv).

If there are not enough values in args to populate the subcommand's required positional parameters, the result is ErrTooFewArgs.

If argument parsing succeeds, Run returns the error produced by calling the subcommand's function, if any.

Types

type Cmd

type Cmd interface {
	// Subcmds returns this Cmd's subcommands as a map,
	// whose keys are subcommand names and values are Subcmd objects.
	// The Commands() function is useful in building this map.
	Subcmds() Map
}

Cmd is a command that has subcommands. It tells Run how to parse its subcommands, and their flags and positional parameters, and how to run them.

type FuncTypeErr added in v2.1.0

type FuncTypeErr struct {
	// Got is the type of the F field.
	Got reflect.Type

	// Want is the expected function type implied by the Params field.
	// Note: there are four variations on this type:
	// variadic vs. non-variadic, and error-returning vs. non-error-returning.
	// FuncTypeErr means a function matched none of those types,
	// but for simplicity Want contains only one of them
	// (the non-variadic, error-returning one).
	Want reflect.Type
}

FuncTypeErr means a Subcmd's F field has a type that does not match the function signature implied by its Params field.

func (FuncTypeErr) Error added in v2.1.0

func (e FuncTypeErr) Error() string

type HelpRequestedErr

type HelpRequestedErr struct {
	// contains filtered or unexported fields
}

HelpRequestedErr is a usage error returned when the "help" pseudo-subcommand-name is used.

func (*HelpRequestedErr) Detail

func (e *HelpRequestedErr) Detail() string

Detail implements Usage.

func (*HelpRequestedErr) Error

func (e *HelpRequestedErr) Error() string

type Map

type Map = map[string]Subcmd

Map is the type of the data structure returned by Cmd.Subcmds and by Commands. It maps a subcommand name to its Subcmd structure.

func Commands

func Commands(args ...interface{}) Map

Commands is a convenience function for producing the Map needed by an implementation of Cmd.Subcmd. It takes arguments in groups of two or four, one group per subcommand.

The first argument of a group is the subcommand's name, a string. The second argument of a group may be a Subcmd, making this a two-argument group.

If it's not a Subcmd, then this is a four-argument group, whose second through fourth arguments are:

  • the function implementing the subcommand;
  • a short description of the subcommand;
  • the list of parameters for the function, a slice of Param (which can be produced with the Params function).

These are used to populate a Subcmd. See Subcmd for a description of the requirements on the implementing function.

A call like this:

Commands(
  "foo", foo, "is the foo subcommand", Params(
    "-verbose", Bool, false, "be verbose",
  ),
  "bar", bar, "is the bar subcommand", Params(
    "-level", Int, 0, "barness level",
  ),
)

is equivalent to:

 Map{
   "foo": Subcmd{
     F:      foo,
     Desc:   "is the foo subcommand",
     Params: []Param{
       {
         Name:    "-verbose",
         Type:    Bool,
         Default: false,
         Doc:     "be verbose",
       },
     },
   },
   "bar": Subcmd{
     F:      bar,
     Desc:   "is the bar subcommand",
     Params: []Param{
       {
         Name:    "-level",
         Type:    Int,
         Default: 0,
         Doc:     "barness level",
       },
     },
   },
}

Note, if a parameter's type is Value, then its default value must be a flag.Value.

This function panics if the number or types of the arguments are wrong.

type MissingSubcmdErr

type MissingSubcmdErr struct {
	// contains filtered or unexported fields
}

MissingSubcmdErr is a usage error returned when Run is called with an empty args list.

func (*MissingSubcmdErr) Detail

func (e *MissingSubcmdErr) Detail() string

Detail implements Usage.

func (*MissingSubcmdErr) Error

func (e *MissingSubcmdErr) Error() string

type Param

type Param struct {
	// Name is the flag name for the parameter.
	// Flags must have a leading "-", as in "-verbose".
	// Positional parameters have no leading "-".
	// Optional positional parameters have a trailing "?", as in "optional?".
	Name string

	// Type is the type of the parameter.
	Type Type

	// Default is a default value for the parameter.
	// Its type must be suitable for Type.
	// If Type is Value,
	// then Default must be a flag.Value.
	Default interface{}

	// Doc is a docstring for the parameter.
	Doc string
}

Param is one parameter of a Subcmd.

func Params

func Params(a ...interface{}) []Param

Params is a convenience function for producing the list of parameters needed by a Subcmd. It takes 4n arguments, where n is the number of parameters. Each group of four is:

  • the name for the parameter, a string (e.g. "-verbose" for a -verbose flag);
  • the type of the parameter, a Type constant;
  • the default value of the parameter,
  • the doc string for the parameter.

Note, if a parameter's type is Value, then its default value must be a flag.Value.

This function panics if the number or types of the arguments are wrong.

func ToFlagSet

func ToFlagSet(params []Param) (fs *flag.FlagSet, ptrs []reflect.Value, positional []Param, err error)

ToFlagSet takes a slice of Param and produces:

  • a flag.FlagSet,
  • a list of properly typed pointers (or in the case of a Value-typed Param, a flag.Value) in which to store the results of calling Parse on the FlagSet,
  • a list of positional Params that are not part of the resulting FlagSet.

On a successful return, len(ptrs)+len(positional) == len(params).

type ParamDefaultErr added in v2.1.0

type ParamDefaultErr struct {
	Param Param
}

ParamDefaultErr is the error when a Param has a default value that is not of the correct type.

func (ParamDefaultErr) Error added in v2.1.0

func (e ParamDefaultErr) Error() string

type ParseErr

type ParseErr struct {
	Err error
}

ParseErr is the type of error returned when parsing a positional parameter according to its type fails.

func (ParseErr) Error

func (e ParseErr) Error() string

func (ParseErr) Unwrap

func (e ParseErr) Unwrap() error

Unwrap unwraps the nested error in e.

type Prefixer added in v2.1.0

type Prefixer interface {
	Prefix() string
}

Prefixer is an optional additional interface that a Cmd can implement. If it does, and a call to Run encounters an unknown subcommand, then before returning an error it will look for an executable in $PATH whose name is Prefix() plus the subcommand name. If it finds one, it is executed with the remaining args as arguments, and a JSON-marshaled copy of the Cmd in the environment variable SUBCMD_ENV (that can be parsed by the subprocess using ParseEnv).

type Subcmd

type Subcmd struct {
	// F is the function implementing the subcommand.
	// Its signature must be one of the following:
	//
	//   - func(context.Context, OPTS, []string)
	//   - func(context.Context, OPTS, []string) error
	//   - func(context.Context, OPTS, ...string)
	//   - func(context.Context, OPTS, ...string) error
	//
	// where OPTS stands for a sequence of zero or more additional parameters
	// corresponding to the types in Params.
	//
	// A Param with type Value supplies a flag.Value to the function.
	// It's up to the function to type-assert the flag.Value to a more-specific type to read the value it contains.
	F interface{}

	// Params describes the parameters to F
	// (excluding the initial context.Context that F takes, and the final []string or ...string).
	Params []Param

	// Desc is a one-line description of this subcommand.
	Desc string
}

Subcmd is one subcommand of a Cmd, and the value type in the Map returned by Cmd.Subcmds.

The function Check can be used to check that the type of the F field is a function with parameters matching those specified by the Params field, and also that each Param has a default value of the correct type.

type Type

type Type int

Type is the type of a Param.

const (
	Bool Type = iota + 1
	Int
	Int64
	Uint
	Uint64
	String
	Float64
	Duration
	Value
)

Possible Param types. These correspond with the types in the standard flag package.

func (Type) String added in v2.1.0

func (t Type) String() string

String returns the name of a Type.

type UnknownSubcmdErr

type UnknownSubcmdErr struct {
	// contains filtered or unexported fields
}

UnknownSubcmdErr is a usage error returned when an unknown subcommand name is passed to Run as args[0].

func (*UnknownSubcmdErr) Detail

func (e *UnknownSubcmdErr) Detail() string

Detail implements Usage.

func (*UnknownSubcmdErr) Error

func (e *UnknownSubcmdErr) Error() string

type UsageErr

type UsageErr interface {
	error
	Detail() string
}

UsageErr is the type of errors that give usage information. Such errors have the usual Error() method producing a one-line string, but also a Detail() method producing a multiline string with more detail.

Jump to

Keyboard shortcuts

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