Documentation ¶
Overview ¶
Package writ implements a flexible option parser with thorough test coverage. It's meant to be simple and "just work". Applications using writ look and behave similar to common GNU command-line applications, making them comfortable for end-users.
Writ implements option decoding with GNU getopt_long conventions. All long and short-form option variations are supported: --with-x, --name Sam, --day=Friday, -i FILE, -vvv, etc.
Help output generation is supported using text/template. The default template can be overriden with a custom template.
Basics ¶
Writ uses the Command and Option types to represent available options and subcommands. Input arguments are decoded with Command.Decode().
For convenience, the New() function can parse an input struct into a Command with Options that represent the input struct's fields. It uses struct field tags to control the behavior. The resulting Command's Decode() method updates the struct's fields in-place when option arguments are decoded.
Alternatively, Commands and Options may be created directly. All fields on these types are exported.
Options ¶
Options are specified via the "option" and "flag" struct tags. Both represent options, but fields marked "option" take arguments, whereas fields marked "flag" do not.
Every Option must have an OptionDecoder. Writ provides decoders for most basic types, as well as some convenience types. See the NewOptionDecoder() function docs for details.
Commands ¶
New() parses an input struct to build a top-level Command. Subcommands are supported by using the "command" field tag. Fields marked with "command" must be of struct type, and are parsed the same way as top-level commands.
Help Output ¶
Writ provides methods for generating help output. Command.WriteHelp() generates help content and writes to a given io.Writer. Command.ExitHelp() writes help content to stdout or stderr and terminates the program.
Writ uses a template to generate the help content. The default template mimics --help output for common GNU programs. See the documentation of the Help type for more details.
Field Tag Reference ¶
The New() function recognizes the following combinations of field tags:
Option Fields: - option (required): a comma-separated list of names for the option - description: the description to display for help output - placeholder: the placeholder value to use next to the option names (e.g. FILE) - default: the default value for the field - env: the name of an environment variable, the value of which is used as a default for the field Flag fields: - flag (required): a comma-separated list of names for the flag - description: the description to display for help output Command fields: - name (required): a name for the command - aliases: a comma-separated list of alias names for the command - description: the description to display for help output
If both "default" and "env" are specified for an option field, the environment variable is consulted first. If the environment variable is present and decodes without error, that value is used. Otherwise, the value for the "default" tag is used. Values specified via parsed arguments take precedence over both types of defaults.
Example (Basic) ¶
This example uses writ.New() to build a command from the Greeter's struct fields. The resulting *writ.Command decodes and updates the Greeter's fields in-place. The Command.ExitHelp() method is used to display help content if --help is specified, or if invalid input arguments are received.
package main import ( "fmt" "github.com/bobziuchkovski/writ" "strings" ) type Greeter struct { HelpFlag bool `flag:"help" description:"Display this help message and exit"` Verbosity int `flag:"v, verbose" description:"Display verbose output"` Name string `option:"n, name" default:"Everyone" description:"The person or people to greet"` } // This example uses writ.New() to build a command from the Greeter's // struct fields. The resulting *writ.Command decodes and updates the // Greeter's fields in-place. The Command.ExitHelp() method is used to // display help content if --help is specified, or if invalid input // arguments are received. func main() { greeter := &Greeter{} cmd := writ.New("greeter", greeter) cmd.Help.Usage = "Usage: greeter [OPTION]... MESSAGE" cmd.Help.Header = "Greet users, displaying MESSAGE" // Use cmd.Decode(os.Args[1:]) in a real application _, positional, err := cmd.Decode([]string{"-vvv", "--name", "Sam", "How's it going?"}) if err != nil || greeter.HelpFlag { cmd.ExitHelp(err) } message := strings.Join(positional, " ") fmt.Printf("Hi %s! %s\n", greeter.Name, message) if greeter.Verbosity > 0 { fmt.Printf("I'm feeling re%slly chatty today!\n", strings.Repeat("a", greeter.Verbosity)) }
Output:
Example (Convenience) ¶
This example demonstrates some of the convenience features offered by writ. It uses writ's support for io types and default values to ensure the Input and Output fields are initialized. These default to stdin and stdout due to the default:"-" field tags. The user may specify -i or -o to read from or write to a file. Similarly, the Replacements map is initialized with key=value pairs for every -r/--replace option the user specifies.
package main import ( "bufio" "errors" "fmt" "github.com/bobziuchkovski/writ" "io" "os" "strings" ) type ReplacerCmd struct { Input io.Reader `option:"i" description:"Read input values from FILE (default: stdin)" default:"-" placeholder:"FILE"` Output io.WriteCloser `option:"o" description:"Write output to FILE (default: stdout)" default:"-" placeholder:"FILE"` Replacements map[string]string `option:"r, replace" description:"Replace occurrences of ORIG with NEW" placeholder:"ORIG=NEW"` HelpFlag bool `flag:"h, help" description:"Display this help text and exit"` } // This example demonstrates some of the convenience features offered by writ. // It uses writ's support for io types and default values to ensure the Input // and Output fields are initialized. These default to stdin and stdout due // to the default:"-" field tags. The user may specify -i or -o to read from // or write to a file. Similarly, the Replacements map is initialized with // key=value pairs for every -r/--replace option the user specifies. func main() { replacer := &ReplacerCmd{} cmd := writ.New("replacer", replacer) cmd.Help.Usage = "Usage: replacer [OPTION]..." cmd.Help.Header = "Perform text replacement according to the -r/--replace option" cmd.Help.Footer = "By default, replacer reads from stdin and write to stdout. Use the -i and -o options to override." // Decode input arguments _, positional, err := cmd.Decode(os.Args[1:]) if err != nil || replacer.HelpFlag { cmd.ExitHelp(err) } if len(positional) > 0 { cmd.ExitHelp(errors.New("replacer does not accept positional arguments")) } // At this point, the ReplacerCmd's Input, Output, and Replacements fields are all // known-valid and initialized, so we can run the replacement. err = replacer.Replace() if err != nil { fmt.Fprintf(os.Stderr, "Error: %s\n", err) os.Exit(1) } } // The Replace() method performs the input/output replacements, but is // not relevant to the example itself. func (r ReplacerCmd) Replace() error { var pairs []string for k, v := range r.Replacements { pairs = append(pairs, k, v) } replacer := strings.NewReplacer(pairs...) scanner := bufio.NewScanner(r.Input) for scanner.Scan() { line := scanner.Text() _, err := io.WriteString(r.Output, replacer.Replace(line)+"\n") if err != nil { return err } } err := scanner.Err() if err != nil { return err } return r.Output.Close() // Help Output: // Usage: replacer [OPTION]... // Perform text replacement according to the -r/--replace option // // Available Options: // -i FILE Read input values from FILE (default: stdin) // -o FILE Write output to FILE (default: stdout) // -r, --replace=ORIG=NEW Replace occurrences of ORIG with NEW // -h, --help Display this help text and exit // // By default, replacer reads from stdin and write to stdout. Use the -i and -o options to override. }
Output:
Example (Explicit) ¶
This example demonstrates explicit Command and Option creation, along with explicit option grouping. It checks the host platform and dynamically adds a --bootloader option if the example is run on Linux. The same result could be achieved by using writ.New() to construct a Command, and then adding the platform-specific option to the resulting Command directly.
package main import ( "github.com/bobziuchkovski/writ" "os" "runtime" ) type Config struct { help bool verbosity int bootloader string } // This example demonstrates explicit Command and Option creation, // along with explicit option grouping. It checks the host platform // and dynamically adds a --bootloader option if the example is run on // Linux. The same result could be achieved by using writ.New() to // construct a Command, and then adding the platform-specific option // to the resulting Command directly. func main() { config := &Config{} cmd := &writ.Command{Name: "explicit"} cmd.Help.Usage = "Usage: explicit [OPTION]... [ARG]..." cmd.Options = []*writ.Option{ { Names: []string{"h", "help"}, Description: "Display this help text and exit", Decoder: writ.NewFlagDecoder(&config.help), Flag: true, }, { Names: []string{"v"}, Description: "Increase verbosity; may be specified more than once", Decoder: writ.NewFlagAccumulator(&config.verbosity), Flag: true, Plural: true, }, } // Note the explicit option grouping. Using writ.New(), a single option group is // created for all options/flags that have descriptions. Without writ.New(), we // need to create the OptionGroup(s) ourselves. general := cmd.GroupOptions("help", "v") general.Header = "General Options:" cmd.Help.OptionGroups = append(cmd.Help.OptionGroups, general) // Dynamically add --bootloader on Linux if runtime.GOOS == "linux" { cmd.Options = append(cmd.Options, &writ.Option{ Names: []string{"bootloader"}, Description: "Use the specified bootloader (grub, grub2, or lilo)", Decoder: writ.NewOptionDecoder(&config.bootloader), Placeholder: "NAME", }) platform := cmd.GroupOptions("bootloader") platform.Header = "Platform Options:" cmd.Help.OptionGroups = append(cmd.Help.OptionGroups, platform) } // Decode the options _, _, err := cmd.Decode(os.Args[1:]) if err != nil || config.help { cmd.ExitHelp(err) } // Help Output, Linux: // General Options: // -h, --help Display this help text and exit // -v Increase verbosity; may be specified more than once // // Platform Options: // --bootloader=NAME Use the specified bootloader (grub, grub2, or lilo) // // Help Output, other platforms: // General Options: // -h, --help Display this help text and exit // -v Increase verbosity; may be specified more than once }
Output:
Example (Subcommand) ¶
This example demonstrates subcommands in a busybox style. There's no requirement that subcommands implement the Run() method shown here. It's just an example of how subcommands might be implemented.
package main import ( "errors" "github.com/bobziuchkovski/writ" "os" ) type GoBox struct { Link Link `command:"ln" alias:"link" description:"Create a soft or hard link"` List List `command:"ls" alias:"list" description:"List directory contents"` } type Link struct { HelpFlag bool `flag:"h, help" description:"Display this message and exit"` Symlink bool `flag:"s" description:"Create a symlink instead of a hard link"` } type List struct { HelpFlag bool `flag:"h, help" description:"Display this message and exit"` LongFormat bool `flag:"l" description:"Use long-format output"` } func (g *GoBox) Run(p writ.Path, positional []string) { // The user didn't specify a subcommand. Give them help. p.Last().ExitHelp(errors.New("COMMAND is required")) } func (l *Link) Run(p writ.Path, positional []string) { if l.HelpFlag { p.Last().ExitHelp(nil) } if len(positional) != 2 { p.Last().ExitHelp(errors.New("ln requires two arguments, OLD and NEW")) } // Link operation omitted for brevity. This would be os.Link or os.Symlink // based on the l.Symlink value. } func (l *List) Run(p writ.Path, positional []string) { if l.HelpFlag { p.Last().ExitHelp(nil) } // Listing operation omitted for brevity. This would be a call to ioutil.ReadDir // followed by conditional formatting based on the l.LongFormat value. } // This example demonstrates subcommands in a busybox style. There's no requirement // that subcommands implement the Run() method shown here. It's just an example of // how subcommands might be implemented. func main() { gobox := &GoBox{} cmd := writ.New("gobox", gobox) cmd.Help.Usage = "Usage: gobox COMMAND [OPTION]... [ARG]..." cmd.Subcommand("ln").Help.Usage = "Usage: gobox ln [-s] OLD NEW" cmd.Subcommand("ls").Help.Usage = "Usage: gobox ls [-l] [PATH]..." path, positional, err := cmd.Decode(os.Args[1:]) if err != nil { // Using path.Last() here ensures the user sees relevant help for their // command selection path.Last().ExitHelp(err) } // At this point, cmd.Decode() has already decoded option values into the gobox // struct, including subcommand values. We just need to dispatch the command. // path.String() is guaranteed to represent the user command selection. switch path.String() { case "gobox": gobox.Run(path, positional) case "gobox ln": gobox.Link.Run(path, positional) case "gobox ls": gobox.List.Run(path, positional) default: panic("BUG: Someone added a new command and forgot to add it's path here") } // Help output, gobox: // Usage: gobox COMMAND [OPTION]... [ARG]... // // Available Commands: // ln Create a soft or hard link // ls List directory contents // // Help output, gobox ln: // Usage: gobox ln [-s] OLD NEW // // Available Options: // -h, --help Display this message and exit // -s Create a symlink instead of a hard link // // Help output, gobox ls: // Usage: gobox ls [-l] [PATH]... // // Available Options: // -h, --help Display this message and exit // -l Use long-format output }
Output:
Index ¶
- Constants
- Variables
- type Command
- func (c *Command) Decode(args []string) (path Path, positional []string, err error)
- func (c *Command) ExitHelp(err error)
- func (c *Command) GroupCommands(names ...string) CommandGroup
- func (c *Command) GroupOptions(names ...string) OptionGroup
- func (c *Command) Option(name string) *Option
- func (c *Command) String() string
- func (c *Command) Subcommand(name string) *Command
- func (c *Command) WriteHelp(w io.Writer) error
- type CommandGroup
- type Help
- type Option
- type OptionDecoder
- type OptionDefaulter
- type OptionGroup
- type Path
Examples ¶
Constants ¶
const HelpText = `` /* 1337-byte string literal not displayed */
HelpText is used by Command.WriteHelp() and Command.ExitHelp() to generate help content.
This copy of the template is used when compiling with go 1.6+. See template_legacy.go for the go < 1.6 template. Output is the same for both templates, but this one is easier to read.
Variables ¶
var Version = struct { Major int Minor int Patch int }{0, 8, 9}
Version records the writ package version.
Functions ¶
This section is empty.
Types ¶
type Command ¶
type Command struct { // Required Name string // Optional Aliases []string Options []*Option Subcommands []*Command Help Help Description string // Commands without descriptions are hidden }
Command specifies program options and subcommands.
NOTE: If building a *Command directly without New(), the Help output will be empty by default. Most applications will want to set the Help.Usage and Help.CommandGroups / Help.OptionGroups fields as appropriate.
func New ¶
New reads the input spec, searching for fields tagged with "option", "flag", or "command". The field type and tags are used to construct a corresponding Command instance, which can be used to decode program arguments. See the package overview documentation for details.
NOTE: The spec value must be a pointer to a struct.
func (*Command) Decode ¶
Decode parses the given arguments according to GNU getopt_long conventions. It matches Option arguments, both short and long-form, and decodes those arguments with the matched Option's Decoder field. If the Command has associated subcommands, the subcommand names are matched and extracted from the start of the positional arguments.
To avoid ambiguity, subcommand matching terminates at the first unmatched positional argument. Similarly, option names are matched against the command hierarchy as it exists at the point the option is encountered. If command "first" has a subcommand "second", and "second" has an option "foo", then "first second --foo" is valid but "first --foo second" returns an error. If the two commands, "first" and "second", both specify a "bar" option, then "first --bar second" decodes "bar" on "first", whereas "first second --bar" decodes "bar" on "second".
As with GNU getopt_long, a bare "--" argument terminates argument parsing. All arguments after the first "--" argument are considered positional parameters.
func (*Command) ExitHelp ¶
ExitHelp writes help output and terminates the program. If err is nil, the output is written to os.Stdout and the program terminates with a 0 exit code. Otherwise, both the help output and error message are written to os.Stderr and the program terminates with a 1 exit code.
func (*Command) GroupCommands ¶
func (c *Command) GroupCommands(names ...string) CommandGroup
GroupCommands is used to build CommandGroups for help output. It searches the method receiver for the named subcommands and returns a corresponding CommandGroup. If any of the named subcommands are not found, GroupCommands panics.
func (*Command) GroupOptions ¶
func (c *Command) GroupOptions(names ...string) OptionGroup
GroupOptions is used to build OptionGroups for help output. It searches the method receiver for the named options and returns a corresponding OptionGroup. If any of the named options are not found, GroupOptions panics.
func (*Command) Option ¶
Option locates options on the method receiver. It returns a match if any of the receiver's options have a matching name. Otherwise it returns nil. Options are searched only on the method receiver, not any of it's subcommands.
func (*Command) Subcommand ¶
Subcommand locates subcommands on the method receiver. It returns a match if any of the receiver's subcommands have a matching name or alias. Otherwise it returns nil.
type CommandGroup ¶
type CommandGroup struct { Commands []*Command // Optional Name string // Not displayed; for matching purposes within the template Header string // Displayed before the group }
CommandGroup is used to customize help output. It groups related Commands for output. When New() parses an input spec, it creates a single CommandGroup for all parsed commands that have descriptions.
type Help ¶
type Help struct { OptionGroups []OptionGroup CommandGroups []CommandGroup // Optional Template *template.Template // Used to render output Usage string // Short message displayed at the top of output Header string // Displayed after Usage }
The Help type is used for presentation purposes only, and does not affect argument parsing.
The Command.ExitHelp() and Command.WriteHelp() methods execute the template assigned to the Template field, passing the Command as input. If the Template field is nil, the writ package's default template is used.
type Option ¶
type Option struct { // Required Names []string Decoder OptionDecoder // Optional Flag bool // If set, the Option takes no arguments Plural bool // If set, the Option may be specified multiple times Description string // Options without descriptions are hidden Placeholder string // Displayed next to option in help output (e.g. FILE) }
Option specifies program options and flags.
func (*Option) LongNames ¶
LongNames returns a filtered slice of the names that are longer than one rune in length.
func (*Option) ShortNames ¶
ShortNames returns a filtered slice of the names that are exactly one rune in length.
type OptionDecoder ¶
OptionDecoder is used for decoding Option arguments. Every Option must have an OptionDecoder assigned. New() constructs and assigns OptionDecoders automatically for supported field types.
func NewDefaulter ¶
func NewDefaulter(decoder OptionDecoder, defaultArg string) OptionDecoder
NewDefaulter builds an OptionDecoder that implements OptionDefaulter. SetDefault calls decoder.Decode() with the value of defaultArg. If the value fails to decode, SetDefault panics.
func NewEnvDefaulter ¶
func NewEnvDefaulter(decoder OptionDecoder, key string) OptionDecoder
NewEnvDefaulter builds an OptionDecoder that implements OptionDefaulter. SetDefault calls decoder.Decode() with the value of the environment variable named by key. If the environment variable isn't set or fails to decode, SetDefault checks if decoder implements OptionDefault. If so, SetDefault calls decoder.SetDefault(). Otherwise, no action is taken.
func NewFlagAccumulator ¶
func NewFlagAccumulator(val *int) OptionDecoder
NewFlagAccumulator builds an OptionDecoder for int flag values. The int value is incremented every time the option is decoded.
func NewFlagDecoder ¶
func NewFlagDecoder(val *bool) OptionDecoder
NewFlagDecoder builds an OptionDecoder for boolean flag values. The boolean value is set when the option is decoded.
func NewOptionDecoder ¶
func NewOptionDecoder(val interface{}) OptionDecoder
NewOptionDecoder builds an OptionDecoder for supported value types. The val parameter must be a pointer to one of the following supported types:
int, int8, int16, int32, int64, uint, uint8, iunt16, uint32, uint64 float32, float64 string, []string map[string]string Argument must be in key=value format. io.Reader, io.ReadCloser Argument must be a path to an existing file, or "-" to specify os.Stdin io.Writer, io.WriteCloser Argument will be used to create a new file, or "-" to specify os.Stdout. If a file already exists at the path specified, it will be overwritten.
type OptionDefaulter ¶
type OptionDefaulter interface {
SetDefault()
}
OptionDefaulter initializes option values to defaults. If an OptionDecoder implements the OptionDefaulter interface, its SetDefault() method is called prior to decoding options.
type OptionGroup ¶
type OptionGroup struct { Options []*Option // Optional Name string // Not displayed; for matching purposes within the template Header string // Displayed before the group }
OptionGroup is used to customize help output. It groups related Options for output. When New() parses an input spec, it creates a single OptionGroup for all parsed options that have descriptions.
type Path ¶
type Path []*Command
Path represents a parsed Command list as returned by Command.Decode(). It is used to differentiate between user selection of commands and subcommands.
func (Path) First ¶
First returns the first command of the path. This is the top-level/root command where Decode() was invoked.