cmdr

package module
v0.3.5 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2024 License: Apache-2.0 Imports: 17 Imported by: 4

README

cmdr -- urfave/cli command line builder

cmdr (a "Commander" tool), is a toolkit for quickly and ergonomically building CLI tools, using a clean and opinionated platform including:

The Commander, Flag interfaces provide a great deal of flexibility for defining and building commands either using a declarative (e.g. structures of options,) or programatically using a chain-able interface and builders.

The top-level MakeRootCommander() constructor also initializes a service orchestration framework using components of github.com/tychoish/fun/srv for service orchestration.

Consider the following example program:

package main

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"sync/atomic"
	"syscall"
	"time"

	"github.com/tychoish/cmdr"
	"github.com/tychoish/fun/srv"
	"github.com/tychoish/grip"
	"github.com/urfave/cli/v2"
)

type ServiceConfig struct {
	Message string
	Timeout time.Duration
}

func StartService(ctx context.Context, conf *ServiceConfig) error {
	// a simple web server

	counter := &atomic.Int64{}
	web := &http.Server{
		Addr: "127.0.0.1:9001",
		Handler: http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

			num := counter.Add(1)

			grip.Infof("got request: %d", num)

			rw.Write([]byte(conf.Message))
		}),
	}

	// cleanup functions run as soon as the context is canceled.
	srv.AddCleanup(ctx, func(context.Context) error {
		grip.Info("beginning cleanup")
		return nil
	})

	grip.Infof("starting web service, pid=%d", os.Getpid())

	return srv.GetOrchestrator(ctx).Add(srv.HTTP("hello-world", time.Minute, web))
}

func BuildCommand() *cmdr.Commander {
	// initialize flag with default value
	msgOpt := cmdr.FlagBuilder("hello world").
		SetName("message", "m").
		SetUsage("message returned by handler")

	timeoutOpt := cmdr.FlagBuilder(time.Hour).
		SetName("timeout", "t").
		SetUsage("timeout for service lifecycle")

	// create an operation spec; initialize the builder with the
	// constructor for the configuration type. While you can use
	// the commander directly and have more access to the
	// cli.Context for interacting with command line arguments,
	// the Spec model makes it possible to write more easily
	// testable functions, and limit your exposure to the CLI
	operation := cmdr.SpecBuilder(func(ctx context.Context, cc *cli.Context) (*ServiceConfig, error) {
		return &ServiceConfig{Message: fmt.Sprintln(cc.String("message"))}, nil
	}).SetMiddleware(func(ctx context.Context, conf *ServiceConfig) context.Context {
		// create a new context with a timeout
		ctx, cancel := context.WithTimeout(ctx, conf.Timeout)

		// this is maybe not meaningful, but means that we
		// cancel this timeout during shutdown and means that
		// we cancel this context during shut down and
		// therefore cannot leak it.
		srv.AddCleanup(ctx, func(context.Context) error { cancel(); return nil })

		// this context is passed to all subsequent options.
		return ctx
	}).SetAction(StartService)

	// build a commander. The root Commander adds service
	// orchestration to the context and manages the lifecylce of
	// services started by commands.
	cmd := cmdr.MakeRootCommander()

	// this that the service will wait for the srv.Orchestrator's
	// services to return rather than canceling the context when
	// the action runs.
	cmd.SetBlocking(true)

	// add flags to Commander
	cmd.Flags(msgOpt.Flag(), timeoutOpt.Flag())

	// add operation to Commander
	cmdr.AddOperationSpec(cmd, operation)

	// return the operation
	return cmd
}

func main() {
	// because the build command is blocking this context means
	// that we'll catch and handle the sig term correctly.
	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
	defer cancel()

	// run the command
	cmdr.Main(ctx, BuildCommand())
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNotDefined = errors.New("not defined")
View Source
var ErrNotSet = errors.New("not set")
View Source
var ErrNotSpecified = errors.New("not specified")

Functions

func GetFlag

func GetFlag[T FlagTypes](cc *cli.Context, name string) T

GetFlag resolves a flag of the specified name to the type as specified.

This will panic at runtime if the type of the flag specified does not match the type of the flag as defined.

func Main

func Main(ctx context.Context, c *Commander)

Main provides an alternative to Run() for calling within in a program's main() function. Non-nil errors are logged at the "Emergency" level and os.Exit(1) is called.

func Run

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

Run executes a commander with the specified command line arguments.

Types

type Action

type Action func(ctx context.Context, c *cli.Context) error

Action defines the core functionality for a command line entry point or handler, providing both the process' context (managed by the commander,) as well as the pre-operation hooks/validation hooks.

Upon execution these functions get the context processed by the middleware, and the cli package's context. In practice, rather than defining action functions directly, use the AddOperation function to define more strongly typed operations.

type AppOptions

type AppOptions struct {
	Usage   string
	Name    string
	Version string
}

AppOptions provides the structure for construction a cli.App from a commander.

type CommandOptions

type CommandOptions[T any] struct {
	Name       string
	Usage      string
	Hook       Hook[T]
	Operation  Operation[T]
	Flags      []Flag
	Middleware func(context.Context, T) context.Context
	Hidden     bool
	Subcommand bool
}

CommandOptions are the arguments to create a sub-command in a commander.

func (CommandOptions[T]) Add

func (opts CommandOptions[T]) Add(c *Commander)

Add modifies the provided commander with the options and operation defined by the CommandOptions. Use in combination with the Command.With method.

type Commander

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

Commander provides a chainable and ergonomic way of defining a command.

The Commander objects largely mirror the semantics of the underlying cli library, which handles execution at runtime. Future versions may use different underlying tools.

Commander provides a strong integration with the github.com/tychoish/fun/srv package's service orchestration framework. A service orchestrator is created and runs during the execution of the program and users can add services and rely on Commander to shut down the orchestrator service and wait for running services to return before returning.

Commanders provide an integrated and strongly typed method for defining setup and configuration before running the command itself. For cleanup after the main operation finishes use the github.com/tychoish/fun/srv package's srv.AddCleanupHook() and srv.AddCleanupError().

func AddOperation

func AddOperation[T any](c *Commander, hook Hook[T], op Operation[T], flags ...Flag) *Commander

AddOperation uses generics to create a hook/operation pair that splits interacting with the cli.Context from the core operation: the Hook creates an object--likely a structure--with the data from the cli args, while the operation can use that structure for the core business logic of the entry point.

An optional number of flags can be added to the command as well.

This form for producing operations separates the parsing of inputs from the operation should serve to make these operations easier to test.

func AddOperationSpec

func AddOperationSpec[T any](c *Commander, s *OperationSpec[T]) *Commander

AddOperationSpec adds an operation to a Commander (and returns the commander.) The OperationSpec makes it possible to define (most) of the operation using strongly typed operations, while passing state directly. A single object of the type T is captured between the function calls.

Because the operation spec builds hooks, middleware, and operations and adds these functions to the convert, it's possible to use AddOperationSpec more than once on a single command. However, there is only ever one action on a commander, so the last non-nil Action specified will be used.

func MakeCommander

func MakeCommander() *Commander

MakeCommander constructs and initializes a command builder object.

func MakeRootCommander

func MakeRootCommander() *Commander

MakeRootCommander constructs a root commander object with basic services configured. From the tychoish/fun/srv package, this pre-populates a base context, shutdown signal, service orchestrator, and cleanup system.

Use MakeCommander to create a commander without these services enabled/running.

func OptionsCommander

func OptionsCommander[T any](opts CommandOptions[T]) *Commander

OptionsCommander creates a new commander as a sub-command returning the new subcommander. Typically you could use this as:

c := cmdr.MakeRootCommand().
       Commander(OptionsCommander(optsOne).SetName("one")).
       Commander(OptionsCommander(optsTwo).SetName("two"))

func Subcommander

func Subcommander[T any](c *Commander, hook Hook[T], op Operation[T], flags ...Flag) *Commander

Subcommander uses the same AddOperation semantics and form, but creates a new Commander adds the operation to that commander, and then adds the subcommand to the provided commander, returning the new sub-commander.

func (*Commander) Aliases

func (c *Commander) Aliases(a ...string) *Commander

func (*Commander) App

func (c *Commander) App() *cli.App

App resolves a command object from the commander and the provided options. You must set the context on the Commander using the setContext before calling this command directly.

In most cases you will use the Run() or Main() methods, rather than App() to use the commander, and Run()/Main() provide their own contexts.

func (*Commander) Command

func (c *Commander) Command() *cli.Command

Command resolves the commander into a cli.Command instance. This operation is safe to call more options.

You should only call this function *after* setting the context on the commander.

func (*Commander) Flags

func (c *Commander) Flags(flags ...Flag) *Commander

func (*Commander) Hooks

func (c *Commander) Hooks(op ...Action) *Commander

Hooks adds a new hook to the commander. Hooks are all executed before the command runs. While all hooks run and errors are collected, if any hook errors the action will not execute.

func (*Commander) Middleware

func (c *Commander) Middleware(mws ...Middleware) *Commander

SetMiddlware allows users to modify the context passed to the hooks and actions of a command.

func (*Commander) SetAction

func (c *Commander) SetAction(in Action) *Commander

SetAction defines the core operation for the commander.

func (*Commander) SetAppOptions

func (c *Commander) SetAppOptions(opts AppOptions) *Commander

SetAppOptions set's the commander's options. This is only used by the top-level root commands.

func (*Commander) SetBlocking

func (c *Commander) SetBlocking(b bool) *Commander

SetBlocking configures the blocking semantics of the command. This setting is only used by root Commander objects. It defaults to false, which means that the action function returns the context passed to services will be canceled.

When true, commanders do not cancel the context after the Action function returns, including for relevant sub commands; instead waiting for any services, managed by the Commanders' orchestrator to return, for the services to signal shutdown, or the context passed to the cmdr.Run or cmdr.Main functions to expire.

func (*Commander) SetName

func (c *Commander) SetName(n string) *Commander

func (*Commander) SetUsage

func (c *Commander) SetUsage(u string) *Commander

func (*Commander) Subcommanders

func (c *Commander) Subcommanders(subs ...*Commander) *Commander

Subcommanders adds a subcommander, returning the original parent commander object.

func (*Commander) UrfaveCommands

func (c *Commander) UrfaveCommands(cc ...*cli.Command) *Commander

UrfaveCommands directly adds a urfae/cli.Command as a subcommand to the Commander.

Commanders do not modify the raw subcommands added in this way, with one exception. Because cli.Command.Action is untyped and it may be reasonable to add Action functions with different signatures, the Commander will attempt to convert common function to `func(*cli.Context) error` functions and avert the error.

Comander will convert Action functions of following types:

func(context.Context) error
func(context.Context, *cli.Context) error
func(context.Context)
func() error
func()

The commander processes the sub commands recursively. All wrapping happens when building the cli.App/cli.Command for the converter, and has limited overhead.

func (*Commander) With

func (c *Commander) With(op func(c *Commander)) *Commander

With makes it possible to embed helper functions in a Commander chain directly.

type Flag

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

Flag defines a command line flag, and is produced using the FlagOptions struct by the MakeFlag function.

func MakeFlag

func MakeFlag[T FlagTypes](opts *FlagOptions[T]) Flag

MakeFlag builds a commandline flag instance and validation from a typed flag to options to a flag object for the command line.

type FlagOptions

type FlagOptions[T FlagTypes] struct {
	Name      string
	Aliases   []string
	Usage     string
	FilePath  string
	Required  bool
	Hidden    bool
	TakesFile bool
	Validate  func(T) error

	TimestampLayout string

	// Default values are provided to the parser for many
	// types. However, slice-types do not support default values.
	Default T
	// Destination provides a pointer to a variable where the flag
	// parser will store the result. The parser only supports this
	// for a subset of types, and this will panic if the type does
	// not support this.
	Destination *T
}

FlagOptions provide a generic way to generate a flag object. Methods on FlagOptions are provided for consistency and ergonomics: they are not safe for concurrent use.

func FlagBuilder

func FlagBuilder[T FlagTypes](defaultVal T) *FlagOptions[T]

FlagBuilder provides a constructor that you can use to build a FlagOptions. Provide the constructor with the default value, which you can override later, if needed. Slice values *must* be the empty list.

func (*FlagOptions[T]) Add

func (fo *FlagOptions[T]) Add(c *Commander)

func (*FlagOptions[T]) AddAliases

func (fo *FlagOptions[T]) AddAliases(a ...string) *FlagOptions[T]

func (*FlagOptions[T]) Flag

func (fo *FlagOptions[T]) Flag() Flag

func (*FlagOptions[T]) SetAliases

func (fo *FlagOptions[T]) SetAliases(a []string) *FlagOptions[T]

func (*FlagOptions[T]) SetDefault

func (fo *FlagOptions[T]) SetDefault(d T) *FlagOptions[T]

func (*FlagOptions[T]) SetDestination

func (fo *FlagOptions[T]) SetDestination(p *T) *FlagOptions[T]

func (*FlagOptions[T]) SetFilePath

func (fo *FlagOptions[T]) SetFilePath(s string) *FlagOptions[T]

func (*FlagOptions[T]) SetHidden

func (fo *FlagOptions[T]) SetHidden(b bool) *FlagOptions[T]

func (*FlagOptions[T]) SetName

func (fo *FlagOptions[T]) SetName(s ...string) *FlagOptions[T]

func (*FlagOptions[T]) SetRequired

func (fo *FlagOptions[T]) SetRequired(b bool) *FlagOptions[T]

func (*FlagOptions[T]) SetTakesFile

func (fo *FlagOptions[T]) SetTakesFile(b bool) *FlagOptions[T]

func (*FlagOptions[T]) SetTimestmapLayout

func (fo *FlagOptions[T]) SetTimestmapLayout(l string) *FlagOptions[T]

func (*FlagOptions[T]) SetUsage

func (fo *FlagOptions[T]) SetUsage(s string) *FlagOptions[T]

func (*FlagOptions[T]) SetValidate

func (fo *FlagOptions[T]) SetValidate(v func(T) error) *FlagOptions[T]

type FlagTypes

type FlagTypes interface {
	string | int | uint | int64 | uint64 | float64 | bool | *time.Time | time.Duration | []string | []int | []int64
}

FlagTypes defines the limited set of types which are supported by the flag parsing system.

type Hook

type Hook[T any] func(context.Context, *cli.Context) (T, error)

Hook generates an object, typically a configuration struct, from the cli.Context provided. Hooks are always processed first, before middleware and the main opreation.

func CompositeHook

func CompositeHook[T any](constr Hook[T], ops ...Operation[T]) Hook[T]

CompositeHook builds a Hook for use with AddOperation and Subcommander that allows for factorking. When T is a mutable type, you can use these composite hooks to process and validate incrementally.

type Middleware

type Middleware func(ctx context.Context) context.Context

Middleware processes the context, attaching timeouts, or values as needed. Middlware is processed after hooks but before the operation.

type Operation

type Operation[T any] func(context.Context, T) error

Operation takes a value, produced by Hook[T], and executes the function.

type OperationSpec

type OperationSpec[T any] struct {
	// Constructor is required and constructs the output object
	// for the operation.
	Constructor Hook[T]
	// The functions in HookOperations are a sequence of
	// type-specialized hooks that you can use to post-process the
	// constructed object, particularly if T is mutable. These run
	// after the constructor during the "hook" phase of the
	// commander.
	HookOperations []Operation[T]
	// Middlware is optional and makes it possible to attach T to
	// a context for later use. Middlewares
	Middleware func(context.Context, T) context.Context
	// Action, the core action.  may be (optionally) specified here as an Operation
	// or directly on the command.
	Action Operation[T]
}

OperationSpec defines a set of functions that The AddOperationSpec functions use to modify a Commander. Unlike using the commander directly, these operations make it possible to

func SpecBuilder

func SpecBuilder[T any](constr Hook[T]) *OperationSpec[T]

SpecBuilder provides an alternate (chainable) method for building an OperationSpec that is consistent with the Commander interface, and that the compiler can correctly infer the correct type for T.

This builder is not safe for concurrent use.

func (*OperationSpec[T]) Add

func (s *OperationSpec[T]) Add(c *Commander)

Add is an option end of a spec builder chain, that adds the chain to the provided Commander. Use this directly or indirectly with the Commander.With method.

func (*OperationSpec[T]) Hooks

func (s *OperationSpec[T]) Hooks(hook ...Operation[T]) *OperationSpec[T]

func (*OperationSpec[T]) SetAction

func (s *OperationSpec[T]) SetAction(op Operation[T]) *OperationSpec[T]

func (*OperationSpec[T]) SetMiddleware

func (s *OperationSpec[T]) SetMiddleware(mw func(context.Context, T) context.Context) *OperationSpec[T]

Jump to

Keyboard shortcuts

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