clingy

package module
v0.0.0-...-57bed7a Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2023 License: CC0-1.0 Imports: 11 Imported by: 7

README

clingy logo

go.dev Go Report Card SourceGraph


Simple example

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/zeebo/clingy"
)

func main() {
	ctx := context.Background()
	ok, err := clingy.Environment{
		Root: new(cmdHello),
	}.Run(ctx, nil)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%+v\n", err)
	}
	if !ok || err != nil {
		os.Exit(1)
	}
}

type cmdHello struct{}

func (c *cmdHello) Setup(params clingy.Parameters) {}
func (c *cmdHello) Execute(ctx context.Context) error {
	fmt.Fprintln(clingy.Stdout(ctx), "Hello world!")
	return nil
}

More complete example

package main

import (
	"context"
	"fmt"
	"os"
	"strconv"

	"github.com/zeebo/clingy"
)

func main() {
	ctx := context.Background()
	ok, err := clingy.Environment{
		Args: []string{"group", "hello", "-s", "zeebo"},
	}.Run(ctx, func(cmds clingy.Commands) {
		cmds.Group("group", "a group of commands", func() {
			cmds.New("hello", "print a greeting", new(cmdHello))
		})
		cmds.New("panic", "oh no! a panic!", new(cmdPanic))
	})
	if err != nil {
		fmt.Fprintf(os.Stderr, "%+v\n", err)
	}
	if !ok || err != nil {
		os.Exit(1)
	}
}

type cmdHello struct {
	name    string
	spanish bool
}

func (c *cmdHello) Setup(params clingy.Parameters) {
	c.spanish = params.Flag("spanish", "greet in spanish", false,
		clingy.Short('s'),
		clingy.Transform(strconv.ParseBool),
		clingy.Boolean,
	).(bool)

	c.name = params.Arg("name", "person to greet").(string)
}

func (c *cmdHello) Execute(ctx context.Context) error {
	if c.spanish {
		fmt.Fprintln(clingy.Stdout(ctx), "Hola", c.name)
	} else {
		fmt.Fprintln(clingy.Stdout(ctx), "Hello", c.name)
	}
	return nil
}

type cmdPanic struct {
	frames int
}

func (c *cmdPanic) Setup(params clingy.Parameters) {
	c.frames = params.Arg("frames", "additional stack frames before panic",
		clingy.Transform(strconv.Atoi),
	).(int)
}

func (c *cmdPanic) Execute(ctx context.Context) error {
	var recur func(int)
	recur = func(x int) {
		if x <= 0 {
			panic("!!")
		}
		recur(x - 1)
	}
	recur(c.frames)
	panic("unreachable")
}

Documentation

Overview

Package clingy is a library to create command line interfaces.

The API is a design experiment that prioritizes discoverability of the code that uses it over anything else. There are two main ways that this can be seen.

First, commands are defined in the same call that is used to execute them. In this way, you cannot define the tree of commands in a bunch of different files in the package, mutating state during init() functions, or otherwise. For example, consider the following execution:

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

ok, err := clingy.Environment{}.Run(ctx, func(cmds clingy.Commands) {
	cmds.Group("files", "Commands related to files", func() {
		cmds.New("copy", "Copy a file", new(cmdFilesCopy))
		cmds.New("delete", "Delete a file", new(cmdFilesDelete))
		cmds.New("read", "Read a file", new(cmdFilesRead))
		cmds.New("list", "List some files", new(cmdFilesList))
	})
	...

Because all of the commands are defined in that single spot, we know exactly which commands are available and where they are defined.

Secondly, flags and arguments for commands are defined at the time they are loaded, and happen as part of a method call on the command implementation itself. This provides incentives to store the state on the command object itself. For example:

type cmdExample struct {
	prefix  string
	verbose bool
	first   string
	second  string
}

func (c *cmdExample) Setup(params clingy.Parameters) {
	c.prefix = params.Flag("prefix", "prefix for output", "example >").(string)
	c.verbose = params.Flag("verbose", "verbose output", false,
		clingy.Transform(strconv.ParseBool), clingy.Boolean).(bool)
	c.first = params.Arg("first", "first required argument").(string)
	c.second = params.Arg("second", "second required argument").(string)
}

func (c *cmdExample) Execute(ctx context.Context) error {
	fmt.Fprintln(clingy.Stdout(ctx), c.prefix, "verbose", c.verbose)
	fmt.Fprintln(clingy.Stdout(ctx), c.prefix, "first", c.first)
	fmt.Fprintln(clingy.Stdout(ctx), c.prefix, "second", c.second)
	return nil
}

This causes the command implementation to be more discoverable because you have a single location that contains all of the state the command will inspect. This also aids in unit testing, because you can construct values directly and execute them, skipping the Setup step and avoiding having to make mocks or fakes to inject the parameters.

In service of this goal, some tradeoffs are made. Most notably, the return type for arguments and flags is `interface{}` and type casts are required. This was found to be acceptable because in typical usage, the argument will be directly assigned into a typed struct field, so the compiler complains if an incorrect type assertion is used. This changes a dynamic failure to a minor redundancy. Additionally, the code with the type assertions is executed right away whenever a command is run, so any testing will surface any bugs.

Enjoy!

Index

Constants

This section is empty.

Variables

View Source
var (
	// Repeated sets the flag or argument to be repeated, returning a slice of values.
	// Repeated arguments must come after any other arguments, Optional or otherwise.
	// If not, New will panic.
	Repeated = Option{func(po *paramOpts) { po.rep = true }}

	// Optional sets the argument to be optional, returning a pointer to a value.
	// Optional arguments must come after any required arguments and before any
	// Repeated arguments. If not, New will panic.
	Optional = Option{func(po *paramOpts) { po.opt = true }}

	// Advanced causes the flag to be hidden unless the --advanced flag is specified
	// when usage information is printed.
	Advanced = Option{func(po *paramOpts) { po.adv = true }}

	// Hidden causes the flag to be hidden when usage information is printed.
	Hidden = Option{func(po *paramOpts) { po.hidden = true }}

	// Boolean causes the flag to be considered a "boolean style" flag where it does
	// not look at the next positional argument if no value is specified.
	Boolean = Option{func(po *paramOpts) { po.bstyle = true }}

	// Required, when passed for the default value of a flag, causes the flag to be
	// required and an error to occur if it is not specified.
	Required = func() interface{} { type anon struct{}; return anon{} }()
)

Functions

func Stderr

func Stderr(ctx context.Context) io.Writer

Stderr returns the io.Writer from the Environment associated to the context.

func Stdin

func Stdin(ctx context.Context) io.Reader

Stdin returns the io.Reader from the Environment associated to the context.

func Stdout

func Stdout(ctx context.Context) io.Writer

Stdout returns the io.Writer from the Environment associated to the context.

Types

type Command

type Command interface {
	// Setup is called to define the positional arguments and flags for the command.
	// The returned values should be stored for the upcoming call to Execute.
	Setup(params Parameters)

	// Execute is called after Setup and should run the command.
	Execute(ctx context.Context) error
}

Command is the interface that executable commands implement.

type Commands

type Commands interface {
	// Flags is embedded to allow one to create global flags.
	Flags

	// New creates a new command.
	New(name, desc string, cmd Command)

	// Group begins a new command group. Calls to New inside of the children
	// function are associated with the most recent call to Group.
	Group(name, desc string, children func())
}

Commands is used to construct the tree of commands and subcommands.

type Environment

type Environment struct {
	// Root specifies the command that is run if no arguments are passed.
	// If nil, the user will be provided with a help screen displaying
	// available commands if no arguments are passed.
	Root Command

	// Name is the name of the binary being executed.
	// If empty, os.Args[0] is used.
	Name string

	// Args is consulted to determine values for the flags and positional arguments.
	// If empty, os.Args[1:] is used.
	Args []string

	// Dynamic, if set, is consulted for global flag values if they are not
	// specified as part of Args. If an error is returned, it will no longer
	// be consulted, and the error will be returned from Run.
	Dynamic func(name string) (vals []string, err error)

	// Wrap, if set, is called with the context and command that would have
	// been executed. The no-op implementation is `return cmd.Execute(ctx)`.
	Wrap func(ctx context.Context, cmd Command) (err error)

	// Getenv, if set, is consulted for querying the process environment.
	// If it is not set, os.Getenv is used.
	Getenv func(key string) string

	// SuggestionsMinEditDistance defines minimum Levenshtein distance to
	// display suggestions when a command/subcommand is misspelled.
	// 0 is the default distance of 2.
	// A negative value disables suggestions.
	SuggestionsMinEditDistance int

	Stdin  io.Reader // Stdin defaults to os.Stdin if unset.
	Stdout io.Writer // Stdout defaults to os.Stdout if unset.
	Stderr io.Writer // Stderr defaults to os.Stderr if unset.
}

Environment is used to control which command is run, what flags and arguments it receives, and what input/output it has access to.

func (Environment) Run

func (env Environment) Run(ctx context.Context, fn func(Commands)) (bool, error)

Run calls the fn to create and execute the tree of commands and global flags. It returns a boolean indicating if the parsing/dispatching of the command was successful. The error is the returned error from any executed command.

type Flags

type Flags interface {
	// Flag creates a new flag. The return value is the value of the flag.
	// If the Repeated option is specified, then the return type is a slice of
	// whatever it would have been. Otherwise, if the Optional option is specified,
	// the return type is a pointer to whatever it would have been. The value provided
	// in def is returned if the flag was not specified. If def is null, then
	// the flag is required, and an error will occur if it is not specified.
	//
	// Flag panics if the same name is defined twice, or if the same Short option
	// is used twice.
	Flag(name, desc string, def interface{}, options ...Option) interface{}

	// Break inserts a line break in the usage output of the flags.
	Break()
}

Flags allows the creation of flags as well as retreiving their values.

type Option

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

Option is the type for values that control details around argument and flags like their presentation, if they are repeated or optional, etc.

func Getenv

func Getenv(key string) Option

Getenv causes the flag to be loaded with the value of the environment variable if not explicitly specified in the argument list.

func Short

func Short(c byte) Option

Short causes the flag to be able to be specified with a single character.

func Transform

func Transform(fns ...interface{}) Option

Transform takes a list of functions meant to parse and transform a string into some final result type. The functions must be of the form (borrowing generics syntax)

func[type T, S any](x T) (y S, err error)

The first function always has an input of type string. The functions are called in sequence. If the argument or flag is Repeated, the functions are called on each element of the values. If Transform is specified multiple times, the functions are appended. In other words, the following two calls are equivalent:

args.New(..., Transform(f1), Transform(f2))
args.New(..., Transform(f1, f2))

func Type

func Type(typ string) Option

Type specifies what type to show in the usage for a flag. For example, specifying

type MyInt int
args.New("foo", "some foo flag", MyInt(5), Type("my_int"), Transform(..)).(MyInt)

would have a usage that looks like

--foo my_int    some foo flag (default 5)

type Parameters

type Parameters interface {
	// Flags is embedded to allow one to create command level flags.
	Flags

	// Arg creates a new argument. The return value is the value of the argument.
	// If the Repeated option is specified, then the return type is a slice of
	// whatever it would have been. Otherwise, if the Optional option is specified,
	// the return type is a pointer to whatever it would have been.
	//
	// Arg panics if the same name is defined twice. Arg panics if any arguments
	// are created after a Repeated argument is created. Arg panics if any arguments
	// that are not Optional or Repeated are created after an Optional argument is
	// created.
	Arg(name, desc string, options ...Option) interface{}
}

Jump to

Keyboard shortcuts

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