Documentation
¶
Overview ¶
Package simplcli is a simple no-dependencies CLI implementation.
A SimplCLI is a collection of sub commands, which are just a function matching the Runner interface and a doc string.
greetRunner := func(ctx context.Context, stdout, stderr io.Writer, args []string) error {
fmt.Fprintln(stdout, "hello!")
return nil
}
cli := simplcli.SimplCLI{
SubCmds: map[string]simplcli.SubCmd{
"hello": {Runner: greetRunner, Doc: "say hello"},
},
}
simplcli.Entrypoint(cli)
Nesting Subcommands ¶
SimplCLI can be nested by using the SimplCLI.Run as the Runner in another SimplCLI's sub commands:
greetRunner := func(ctx context.Context, stdout, stderr io.Writer, args []string) error {
fmt.Fprintln(stdout, "hello, world!")
return nil
}
childCLI := simplcli.SimplCLI{
SubCmds: map[string]simplcli.SubCmd{
"greet": {Runner: greetRunner, Doc: "greet the world"},
},
}
parentCLI := simplcli.SimplCLI{
SubCmds: map[string]simplcli.SubCmd{
"sub": {Runner: childCLI.Run, Doc: "a nested subcommand"},
},
}
err := parentCLI.Run(ctx, os.Stdout, os.Stderr, []string{"sub", "greet"})
Middleware ¶
To facilitate setup/teardown (e.g. database connection, logging, etc.pp) Middleware can be used. Middlewares set on a SimplCLI are executed in order for every sub command.
logMiddleware := func(ctx context.Context, stdout, stderr io.Writer, args []string, next Runner) error {
fmt.Fprintln(stdout, "before command")
err := next(ctx, stdout, stderr, args)
fmt.Fprintln(stdout, "after command")
return err
}
cliWithMiddleware := simplcli.SimplCLI{
Middlewares: []simplcli.Middleware{logMiddleware},
SubCmds: map[string]simplcli.SubCmd{
"greet": {Runner: greetRunner, Doc: "greet someone"},
},
}
err := cliWithMiddleware.Run(ctx, os.Stdout, os.Stderr, []string{"greet"})
Flag handling ¶
Handling flags is not easy - the stdlib flag package is fantastic but it also has drawbacks.
Two approaches that work well with simplcli is either using package level flags and calling flag.Parse in a middleware or using flag.FlagSet.
flag.Parse with package level flags works well for small-ish cli applications that have many shared global values.
var myFlag = flag.String("flag", "default", "docs...")
cli := simplcli.SimplCLI{
Middlewares: []simplcli.Middleware{
func(ctx context.Context, stdout, stderr io.Writer, args []string, next Runner) error {
flag.Parse()
return next(ctx, stdout, stderr, flag.Args())
},
},
SubCmds: map[string]simplcli.SubCmd{
// ...
},
}
simplcli.Entrypoint(cli)
flag.FlagSet has the same features as the package level flags but allows building the flagset for each sub command specifically.
A common pattern is to have a method RegisterFlags on structs, which take a flag.FlagSet and register their flags on it.
type Options struct {
SomeAttribute string
}
func (g *Greeter) RegisterFlags(fs *flags.FlagSet) {
fs.StringVar(&o.SomeAttribute, "", false, "Attribute content")
}
greetRunner := func(ctx context.Context, stdout, stderr io.Writer, args []string) error {
fs := flag.NewFlagSet("", flag.ExitOnError)
fs.SetOutput(stderr)
fName := fs.String("name", "", "Name to greet")
o := &Options{}
o.RegisterFlags(fs)
if err := fs.Parse(args); err != nil {
return err
}
_, err := fmt.Fprintf(stdout, "hello %s!", *fName)
return err
}
cli := simplcli.SimplCLI{
SubCmds: map[string]simplcli.SubCmd{
"greet": {Runner: greetRunner, Doc: "greet someone"},
},
}
simplcli.Entrypoint(cli)
Index ¶
Examples ¶
Constants ¶
const ( // Help is the special "help" subcommand. By default simplcli prints // all available subcommands. If a SimplCLI has a custom help // subcommand set it will be executed instead. // The Help function is a [Runner] but only gets a valid stdout. // io.Discard is passed as stderr. Help = "help" )
Variables ¶
var ( // ErrNoArgs is returned when no arguments are passed. ErrNoArgs = fmt.Errorf(`no arguments passed, pass %q as the first argument `+ `to get the list of available subcommands`, Help) )
Functions ¶
func Entrypoint ¶
func Entrypoint(simplcli SimplCLI)
Entrypoint runs SimplCLI with a signal-cancelled context.Context, os.Stdout, os.Stderr and os.Args. If an error is returned it is printed to os.Stderr and os.Exit is called.
func PrintDefaultHelp ¶
PrintDefaultHelp prints all available subcommands to out.
func WithSignal ¶
func WithSignal(ctx context.Context, stderr io.Writer, signals ...os.Signal) (context.Context, context.CancelFunc)
WithSignal wraps a context.Context to be cancelled when the specified signals are received. This is virtually equivalent to signal.NotifyContext but prints a message to stderr. When no signals are passed os.Interrupt and syscal.SIGTERM are defaulted.
Types ¶
type Middleware ¶
type Middleware func(ctx context.Context, stdout, stderr io.Writer, args []string, next Runner) error
Middleware is a function that wraps Runner.
Example ¶
logMiddleware := func(ctx context.Context, stdout, stderr io.Writer, args []string, next Runner) error {
fmt.Fprintln(stdout, "before command")
err := next(ctx, stdout, stderr, args)
fmt.Fprintln(stdout, "after command")
return err
}
greetRunner := func(_ context.Context, stdout, _ io.Writer, _ []string) error {
fmt.Fprintln(stdout, "hello, world!")
return nil
}
cliWithMiddleware := SimplCLI{
Middlewares: []Middleware{logMiddleware},
SubCmds: map[string]SubCmd{
"greet": {Runner: greetRunner, Doc: "greet someone"},
},
}
if err := cliWithMiddleware.Run(context.Background(), os.Stdout, os.Stderr, []string{"greet"}); err != nil {
log.Fatal(err)
}
Output: before command hello, world! after command
type SimplCLI ¶
type SimplCLI struct {
// The [SubCmd]'s to execute.
SubCmds map[string]SubCmd
// A list of middlewares to be applied to all subcommands.
Middlewares []Middleware
}
SimplCLI contains multiple SubCmd's.
Example ¶
greetRunner := func(_ context.Context, stdout, _ io.Writer, _ []string) error {
fmt.Fprintln(stdout, "hello!")
return nil
}
cli := SimplCLI{
SubCmds: map[string]SubCmd{
"hello": {Runner: greetRunner, Doc: "say hello"},
},
}
if err := cli.Run(context.Background(), os.Stdout, os.Stderr, []string{"hello"}); err != nil {
log.Fatal(err)
}
Output: hello!
func (SimplCLI) PrintHelp ¶
PrintHelp prints the available subcommands to out.
Example ¶
noop := func(_ context.Context, _, _ io.Writer, _ []string) error {
return nil
}
cli := SimplCLI{
SubCmds: map[string]SubCmd{
"first": {noop, "first subcommand"},
"second": {noop, "second subcommand"},
},
}
if err := cli.PrintHelp(context.Background(), os.Stdout); err != nil {
log.Fatal(err)
}
Output: Available subcommands: first first subcommand second second subcommand
func (SimplCLI) Run ¶
Run runs subcommand indicated by the first argument. If the first argument is "help" all registered subcommands are printed to out.
Example ¶
Nested SimplCLI
greetRunner := func(_ context.Context, stdout, _ io.Writer, _ []string) error {
fmt.Fprintln(stdout, "hello, world!")
return nil
}
childCLI := SimplCLI{
SubCmds: map[string]SubCmd{
"greet": {Runner: greetRunner, Doc: "greet the world"},
},
}
parentCLI := SimplCLI{
SubCmds: map[string]SubCmd{
"sub": {Runner: childCLI.Run, Doc: "a nested subcommand"},
},
}
if err := parentCLI.Run(context.Background(), os.Stdout, os.Stderr, []string{"sub", "greet"}); err != nil {
log.Fatal(err)
}
Output: hello, world!