serpent

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Mar 19, 2024 License: CC0-1.0 Imports: 38 Imported by: 12

README

serpent

Go Reference

serpent is a Go CLI configuration framework based on cobra and used by coder/coder. It's designed for large-scale CLIs with dozens of commands and hundreds of options. If you're building a small, self-contained tool, go with cobra.

help example

When compared to cobra, serpent strives for:

  • Better default help output inspired by the Go toolchain
  • Greater flexibility in accepting options that span across multiple sources
  • Composition via middleware
  • Testability (e.g. OS Stdout and Stderr is only available to commands explicitly)

Basic Usage

See example/echo:

package main

import (
	"os"
	"strings"

	"github.com/coder/serpent"
)

func main() {
	var upper bool
	cmd := serpent.Command{
		Use:   "echo <text>",
		Short: "Prints the given text to the console.",
		Options: serpent.OptionSet{
			{
				Name:        "upper",
				Value:       serpent.BoolOf(&upper),
				Flag:        "upper",
				Description: "Prints the text in upper case.",
			},
		},
		Handler: func(inv *serpent.Invocation) error {
			if len(inv.Args) == 0 {
				inv.Stderr.Write([]byte("error: missing text\n"))
				os.Exit(1)
			}

			text := inv.Args[0]
			if upper {
				text = strings.ToUpper(text)
			}

			inv.Stdout.Write([]byte(text))
			return nil
		},
	}

	err := cmd.Invoke().WithOS().Run()
	if err != nil {
		panic(err)
	}
}

Design

This Design section assumes you have a good understanding of how cobra works.

Options

Serpent is designed for high-configurability. To us, that means providing many ways to configure the same value (env, YAML, flags, etc.) and keeping the code clean and testable as you scale the number of options.

Serpent's Option type looks like:

type Option struct {
	Name string
	Flag string
	Env string
	Default string
	Value pflag.Value
	// ...
}

And is used by each Command when passed as an array to the Options field.

More coming...

This README is a stub for now. We'll better explain the design and usage of serpent in the future.

Documentation

Overview

Package serpent offers an all-in-one solution for a highly configurable CLI application. Within Coder, we use it for all of our subcommands, which demands more functionality than cobra/viber offers.

The Command interface is loosely based on the chi middleware pattern and http.Handler/HandlerFunc.

Index

Constants

This section is empty.

Variables

View Source
var DiscardValue discardValue

DiscardValue does nothing but implements the pflag.Value interface. It's useful in cases where you want to accept an option, but access the underlying value directly instead of through the Option methods.

Functions

This section is empty.

Types

type Annotations

type Annotations map[string]string

Annotations is an arbitrary key-mapping used to extend the Option and Command types. Its methods won't panic if the map is nil.

func (Annotations) Get

func (a Annotations) Get(key string) (string, bool)

Get retrieves a key from the map, returning false if the key is not found or the map is nil.

func (Annotations) IsSet

func (a Annotations) IsSet(key string) bool

IsSet returns true if the key is set in the annotations map.

func (Annotations) Mark

func (a Annotations) Mark(key string, value string) Annotations

Mark sets a value on the annotations map, creating one if it doesn't exist. Mark does not mutate the original and returns a copy. It is suitable for chaining.

type Bool

type Bool bool

func BoolOf

func BoolOf(b *bool) *Bool

func (*Bool) NoOptDefValue

func (*Bool) NoOptDefValue() string

func (*Bool) Set

func (b *Bool) Set(s string) error

func (Bool) String

func (b Bool) String() string

func (Bool) Type

func (Bool) Type() string

func (Bool) Value

func (b Bool) Value() bool

type Command added in v0.5.0

type Command struct {
	// Parent is the direct parent of the command.
	//
	// It is set automatically when an invokation runs.
	Parent *Command

	// Children is a list of direct descendants.
	Children []*Command

	// Use is provided in form "command [flags] [args...]".
	Use string

	// Aliases is a list of alternative names for the command.
	Aliases []string

	// Short is a one-line description of the command.
	Short string

	// Hidden determines whether the command should be hidden from help.
	Hidden bool

	// RawArgs determines whether the command should receive unparsed arguments.
	// No flags are parsed when set, and the command is responsible for parsing
	// its own flags.
	RawArgs bool

	// Long is a detailed description of the command,
	// presented on its help page. It may contain examples.
	Long        string
	Options     OptionSet
	Annotations Annotations

	// Middleware is called before the Handler.
	// Use Chain() to combine multiple middlewares.
	Middleware  MiddlewareFunc
	Handler     HandlerFunc
	HelpHandler HandlerFunc
}

Command describes an executable command.

func (*Command) AddSubcommands added in v0.5.0

func (c *Command) AddSubcommands(cmds ...*Command)

AddSubcommands adds the given subcommands, setting their Parent field automatically.

func (*Command) FullName added in v0.5.0

func (c *Command) FullName() string

FullName returns the full invocation name of the command, as seen on the command line.

func (*Command) FullOptions added in v0.5.0

func (c *Command) FullOptions() OptionSet

FullOptions returns the options of the command and its parents.

func (*Command) FullUsage added in v0.5.0

func (c *Command) FullUsage() string

FullName returns usage of the command, preceded by the usage of its parents.

func (*Command) Invoke added in v0.5.0

func (c *Command) Invoke(args ...string) *Invocation

Invoke creates a new invocation of the command, with stdio discarded.

The returned invocation is not live until Run() is called.

func (*Command) Name added in v0.5.0

func (c *Command) Name() string

Name returns the first word in the Use string.

func (*Command) Walk added in v0.5.0

func (c *Command) Walk(fn func(*Command))

Walk calls fn for the command and all its children.

type Duration

type Duration time.Duration

func DurationOf

func DurationOf(d *time.Duration) *Duration

func (*Duration) MarshalYAML

func (d *Duration) MarshalYAML() (interface{}, error)

func (*Duration) Set

func (d *Duration) Set(v string) error

func (*Duration) String

func (d *Duration) String() string

func (Duration) Type

func (Duration) Type() string

func (*Duration) UnmarshalYAML

func (d *Duration) UnmarshalYAML(n *yaml.Node) error

func (*Duration) Value

func (d *Duration) Value() time.Duration

type Enum

type Enum struct {
	Choices []string
	Value   *string
}

func EnumOf

func EnumOf(v *string, choices ...string) *Enum

func (*Enum) MarshalYAML added in v0.6.0

func (e *Enum) MarshalYAML() (interface{}, error)

func (*Enum) Set

func (e *Enum) Set(v string) error

func (*Enum) String

func (e *Enum) String() string

func (*Enum) Type

func (e *Enum) Type() string

func (*Enum) UnmarshalYAML added in v0.6.0

func (e *Enum) UnmarshalYAML(n *yaml.Node) error

type EnvVar

type EnvVar struct {
	Name  string
	Value string
}

Var represents a single environment variable of form NAME=VALUE.

type Environ

type Environ []EnvVar

func ParseEnviron

func ParseEnviron(environ []string, prefix string) Environ

ParseEnviron returns all environment variables starting with prefix without said prefix.

func (Environ) Get

func (e Environ) Get(name string) string

func (Environ) Lookup

func (e Environ) Lookup(name string) (string, bool)

func (*Environ) Set

func (e *Environ) Set(name, value string)

func (Environ) ToOS

func (e Environ) ToOS() []string

type Group

type Group struct {
	Parent      *Group `json:"parent,omitempty"`
	Name        string `json:"name,omitempty"`
	YAML        string `json:"yaml,omitempty"`
	Description string `json:"description,omitempty"`
}

Group describes a hierarchy of groups that an option or command belongs to.

func (*Group) Ancestry

func (g *Group) Ancestry() []Group

Ancestry returns the group and all of its parents, in order.

func (*Group) FullName

func (g *Group) FullName() string

type HandlerFunc

type HandlerFunc func(i *Invocation) error

HandlerFunc handles an Invocation of a command.

type HostPort

type HostPort struct {
	Host string
	Port string
}

HostPort is a host:port pair.

func (*HostPort) MarshalJSON

func (hp *HostPort) MarshalJSON() ([]byte, error)

func (*HostPort) MarshalYAML

func (hp *HostPort) MarshalYAML() (interface{}, error)

func (*HostPort) Set

func (hp *HostPort) Set(v string) error

func (*HostPort) String

func (hp *HostPort) String() string

func (*HostPort) Type

func (*HostPort) Type() string

func (*HostPort) UnmarshalJSON

func (hp *HostPort) UnmarshalJSON(b []byte) error

func (*HostPort) UnmarshalYAML

func (hp *HostPort) UnmarshalYAML(n *yaml.Node) error

type Int64

type Int64 int64

func Int64Of

func Int64Of(i *int64) *Int64

func (*Int64) Set

func (i *Int64) Set(s string) error

func (Int64) String

func (i Int64) String() string

func (Int64) Type

func (Int64) Type() string

func (Int64) Value

func (i Int64) Value() int64

type Invocation

type Invocation struct {
	Command *Command

	Args []string
	// Environ is a list of environment variables. Use EnvsWithPrefix to parse
	// os.Environ.
	Environ Environ
	Stdout  io.Writer
	Stderr  io.Writer
	Stdin   io.Reader
	Logger  slog.Logger
	Net     Net
	// contains filtered or unexported fields
}

Invocation represents an instance of a command being executed.

func (*Invocation) Context

func (inv *Invocation) Context() context.Context

func (*Invocation) ParsedFlags

func (inv *Invocation) ParsedFlags() *pflag.FlagSet

func (*Invocation) Run

func (inv *Invocation) Run() (err error)

Run executes the command. If two command share a flag name, the first command wins.

func (*Invocation) SignalNotifyContext

func (inv *Invocation) SignalNotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)

SignalNotifyContext is equivalent to signal.NotifyContext, but supports being overridden in tests.

func (*Invocation) WithContext

func (inv *Invocation) WithContext(ctx context.Context) *Invocation

WithContext returns a copy of the Invocation with the given context.

func (*Invocation) WithOS

func (inv *Invocation) WithOS() *Invocation

WithOS returns the invocation as a main package, filling in the invocation's unset fields with OS defaults.

func (*Invocation) WithTestParsedFlags

func (inv *Invocation) WithTestParsedFlags(
	_ testing.TB,
	parsedFlags *pflag.FlagSet,
) *Invocation

func (*Invocation) WithTestSignalNotifyContext

func (inv *Invocation) WithTestSignalNotifyContext(
	_ testing.TB,
	f func(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc),
) *Invocation

WithTestSignalNotifyContext allows overriding the default implementation of SignalNotifyContext. This should only be used in testing.

type MiddlewareFunc

type MiddlewareFunc func(next HandlerFunc) HandlerFunc

MiddlewareFunc returns the next handler in the chain, or nil if there are no more.

func Chain

func Chain(ms ...MiddlewareFunc) MiddlewareFunc

Chain returns a Handler that first calls middleware in order.

func RequireNArgs

func RequireNArgs(want int) MiddlewareFunc

func RequireRangeArgs

func RequireRangeArgs(start, end int) MiddlewareFunc

RequireRangeArgs returns a Middleware that requires the number of arguments to be between start and end (inclusive). If end is -1, then the number of arguments must be at least start.

type Net

type Net interface {
	// Listen has the same semantics as `net.Listen` but also supports `udp`
	Listen(network, address string) (net.Listener, error)
}

Net abstracts CLI commands interacting with the operating system networking.

At present, it covers opening local listening sockets, since doing this in testing is a challenge without flakes, since it's hard to pick a port we know a priori will be free.

type NoOptDefValuer

type NoOptDefValuer interface {
	NoOptDefValue() string
}

NoOptDefValuer describes behavior when no option is passed into the flag.

This is useful for boolean or otherwise binary flags.

type Option

type Option struct {
	Name        string `json:"name,omitempty"`
	Description string `json:"description,omitempty"`
	// Required means this value must be set by some means. It requires
	// `ValueSource != ValueSourceNone`
	// If `Default` is set, then `Required` is ignored.
	Required bool `json:"required,omitempty"`

	// Flag is the long name of the flag used to configure this option. If unset,
	// flag configuring is disabled.
	Flag string `json:"flag,omitempty"`
	// FlagShorthand is the one-character shorthand for the flag. If unset, no
	// shorthand is used.
	FlagShorthand string `json:"flag_shorthand,omitempty"`

	// Env is the environment variable used to configure this option. If unset,
	// environment configuring is disabled.
	Env string `json:"env,omitempty"`

	// YAML is the YAML key used to configure this option. If unset, YAML
	// configuring is disabled.
	YAML string `json:"yaml,omitempty"`

	// Default is parsed into Value if set.
	Default string `json:"default,omitempty"`
	// Value includes the types listed in values.go.
	Value pflag.Value `json:"value,omitempty"`

	// Annotations enable extensions to serpent higher up in the stack. It's useful for
	// help formatting and documentation generation.
	Annotations Annotations `json:"annotations,omitempty"`

	// Group is a group hierarchy that helps organize this option in help, configs
	// and other documentation.
	Group *Group `json:"group,omitempty"`

	// UseInstead is a list of options that should be used instead of this one.
	// The field is used to generate a deprecation warning.
	UseInstead []Option `json:"use_instead,omitempty"`

	Hidden bool `json:"hidden,omitempty"`

	ValueSource ValueSource `json:"value_source,omitempty"`
}

Option is a configuration option for a CLI application.

func (*Option) UnmarshalJSON

func (o *Option) UnmarshalJSON(data []byte) error

func (Option) YAMLPath

func (o Option) YAMLPath() string

type OptionSet

type OptionSet []Option

OptionSet is a group of options that can be applied to a command.

func (*OptionSet) Add

func (optSet *OptionSet) Add(opts ...Option)

Add adds the given Options to the OptionSet.

func (*OptionSet) ByName

func (optSet *OptionSet) ByName(name string) *Option

ByName returns the Option with the given name, or nil if no such option exists.

func (OptionSet) Filter

func (optSet OptionSet) Filter(filter func(opt Option) bool) OptionSet

Filter will only return options that match the given filter. (return true)

func (*OptionSet) FlagSet

func (optSet *OptionSet) FlagSet() *pflag.FlagSet

FlagSet returns a pflag.FlagSet for the OptionSet.

func (*OptionSet) MarshalYAML

func (optSet *OptionSet) MarshalYAML() (any, error)

MarshalYAML converts the option set to a YAML node, that can be converted into bytes via yaml.Marshal.

The node is returned to enable post-processing higher up in the stack.

It is isomorphic with FromYAML.

func (*OptionSet) ParseEnv

func (optSet *OptionSet) ParseEnv(vs []EnvVar) error

ParseEnv parses the given environment variables into the OptionSet. Use EnvsWithPrefix to filter out prefixes.

func (*OptionSet) SetDefaults

func (optSet *OptionSet) SetDefaults() error

SetDefaults sets the default values for each Option, skipping values that already have a value source.

func (*OptionSet) UnmarshalJSON

func (optSet *OptionSet) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler for OptionSets. Options have an interface Value type that cannot handle unmarshalling because the types cannot be inferred. Since it is a slice, instantiating the Options first does not help.

However, we typically do instantiate the slice to have the correct types. So this unmarshaller will attempt to find the named option in the existing set, if it cannot, the value is discarded. If the option exists, the value is unmarshalled into the existing option, and replaces the existing option.

The value is discarded if it's type cannot be inferred. This behavior just feels "safer", although it should never happen if the correct option set is passed in. The situation where this could occur is if a client and server are on different versions with different options.

func (*OptionSet) UnmarshalYAML

func (optSet *OptionSet) UnmarshalYAML(rootNode *yaml.Node) error

UnmarshalYAML converts the given YAML node into the option set. It is isomorphic with ToYAML.

type Regexp

type Regexp regexp.Regexp

func (*Regexp) MarshalJSON

func (r *Regexp) MarshalJSON() ([]byte, error)

func (*Regexp) MarshalYAML

func (r *Regexp) MarshalYAML() (interface{}, error)

func (*Regexp) Set

func (r *Regexp) Set(v string) error

func (Regexp) String

func (r Regexp) String() string

func (Regexp) Type

func (Regexp) Type() string

func (*Regexp) UnmarshalJSON

func (r *Regexp) UnmarshalJSON(data []byte) error

func (*Regexp) UnmarshalYAML

func (r *Regexp) UnmarshalYAML(n *yaml.Node) error

func (*Regexp) Value

func (r *Regexp) Value() *regexp.Regexp

type RunCommandError

type RunCommandError struct {
	Cmd *Command
	Err error
}

func (*RunCommandError) Error

func (e *RunCommandError) Error() string

func (*RunCommandError) Unwrap

func (e *RunCommandError) Unwrap() error

type String

type String string

func StringOf

func StringOf(s *string) *String

func (*String) NoOptDefValue

func (*String) NoOptDefValue() string

func (*String) Set

func (s *String) Set(v string) error

func (String) String

func (s String) String() string

func (String) Type

func (String) Type() string

func (String) Value

func (s String) Value() string

type StringArray

type StringArray []string

StringArray is a slice of strings that implements pflag.Value and pflag.SliceValue.

func StringArrayOf

func StringArrayOf(ss *[]string) *StringArray

func (*StringArray) Append

func (s *StringArray) Append(v string) error

func (*StringArray) GetSlice

func (s *StringArray) GetSlice() []string

func (*StringArray) Replace

func (s *StringArray) Replace(vals []string) error

func (*StringArray) Set

func (s *StringArray) Set(v string) error

func (StringArray) String

func (s StringArray) String() string

func (StringArray) Type

func (StringArray) Type() string

func (StringArray) Value

func (s StringArray) Value() []string

type Struct

type Struct[T any] struct {
	Value T
}

Struct is a special value type that encodes an arbitrary struct. It implements the flag.Value interface, but in general these values should only be accepted via config for ergonomics.

The string encoding type is YAML.

func (*Struct[T]) MarshalJSON

func (s *Struct[T]) MarshalJSON() ([]byte, error)

nolint:revive

func (*Struct[T]) MarshalYAML

func (s *Struct[T]) MarshalYAML() (interface{}, error)

nolint:revive

func (*Struct[T]) Set

func (s *Struct[T]) Set(v string) error

func (*Struct[T]) String

func (s *Struct[T]) String() string

func (*Struct[T]) Type

func (s *Struct[T]) Type() string

func (*Struct[T]) UnmarshalJSON

func (s *Struct[T]) UnmarshalJSON(b []byte) error

nolint:revive

func (*Struct[T]) UnmarshalYAML

func (s *Struct[T]) UnmarshalYAML(n *yaml.Node) error

nolint:revive

type URL

type URL url.URL

func URLOf

func URLOf(u *url.URL) *URL

func (*URL) MarshalJSON

func (u *URL) MarshalJSON() ([]byte, error)

func (*URL) MarshalYAML

func (u *URL) MarshalYAML() (interface{}, error)

func (*URL) Set

func (u *URL) Set(v string) error

func (*URL) String

func (u *URL) String() string

func (*URL) Type

func (*URL) Type() string

func (*URL) UnmarshalJSON

func (u *URL) UnmarshalJSON(b []byte) error

func (*URL) UnmarshalYAML

func (u *URL) UnmarshalYAML(n *yaml.Node) error

func (*URL) Value

func (u *URL) Value() *url.URL

type Validator

type Validator[T pflag.Value] struct {
	Value T
	// contains filtered or unexported fields
}

Validator is a wrapper around a pflag.Value that allows for validation of the value after or before it has been set.

func Validate

func Validate[T pflag.Value](opt T, validate func(value T) error) *Validator[T]

func (*Validator[T]) MarshalJSON

func (i *Validator[T]) MarshalJSON() ([]byte, error)

func (*Validator[T]) MarshalYAML

func (i *Validator[T]) MarshalYAML() (interface{}, error)

func (*Validator[T]) Set

func (i *Validator[T]) Set(input string) error

func (*Validator[T]) String

func (i *Validator[T]) String() string

func (*Validator[T]) Type

func (i *Validator[T]) Type() string

func (*Validator[T]) Underlying

func (i *Validator[T]) Underlying() pflag.Value

func (*Validator[T]) UnmarshalJSON

func (i *Validator[T]) UnmarshalJSON(b []byte) error

func (*Validator[T]) UnmarshalYAML

func (i *Validator[T]) UnmarshalYAML(n *yaml.Node) error

type ValueSource

type ValueSource string
const (
	ValueSourceNone    ValueSource = ""
	ValueSourceFlag    ValueSource = "flag"
	ValueSourceEnv     ValueSource = "env"
	ValueSourceYAML    ValueSource = "yaml"
	ValueSourceDefault ValueSource = "default"
)

type YAMLConfigPath

type YAMLConfigPath string

YAMLConfigPath is a special value type that encodes a path to a YAML configuration file where options are read from.

func (*YAMLConfigPath) Set

func (p *YAMLConfigPath) Set(v string) error

func (*YAMLConfigPath) String

func (p *YAMLConfigPath) String() string

func (*YAMLConfigPath) Type

func (*YAMLConfigPath) Type() string

Directories

Path Synopsis
example

Jump to

Keyboard shortcuts

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