Writ: A friendly option parser that "just works"

Hi Gophers,

I’m pleased to announce the release of Writ, an easy-to-use option parser that “just works”: https://github.com/ziuchkovski/writ. It implements GNU getopt_long conventions, has thorough test coverage (98.7%), and handles all of the the tricky corner cases, such as bare “-” and “–” arguments.

Writ decodes options similar to encoding/json or encoding/xml. Just pass a struct with “option”, “flag”, and “command” field tags, and it decodes the input arguments appropriately.

Help output generation is offered for convenience, but isn’t required. The default output closely mimics formatting of --help output for common GNU commands.

I hope it’s a helpful tool for those of you writing command-line apps!

Bob

5 Likes

This is great work, and I appreciate the solid README examples.

I’ve used other popular CLI libs before and they didn’t gel with what I wanted: this seems to be a great mix of “just enough boilerplate” with a compact API surface.

Thanks for checking it out, and thanks for the feedback!

Let me know if you find any parts of Writ that don’t gel. I really am trying to find that balance of flexibility and convenience.

Random suggestion :slight_smile: … if you want to a extra convenience API:

package main

import (
    "fmt"
    "github.com/ziuchkovski/writ"
    "strings"
)

var Config struct {
    *writ.Command

    HelpFlag  bool   `flag:"help" description:"display this help message"`
    Verbosity int    `flag:"v, verbose" description:"display verbose output"`
    Name      string `option:"n, name" default:"Everyone" description:"the person to greet"`

    Sub struct {
        // *writ.Command // optional
        HelpFlag  bool   `flag:"help" description:"display sub command help message"`
    } `command:"sub"`
}

func main() {
    writ.Quick(&Config)
    Config.Help.Usage = "Usage: greeter [OPTION]..."
    Config.Help.Header = "..."
    Config.Help.Footer = "..."

    fmt.Println(Config.HelpFlag)
    fmt.Println(Config.Verbosity)
    fmt.Println(Config.Name)
    fmt.Println(Config.Sub.HelpFlag)
}

it might already work to some degree, I didn’t test :slight_smile: . Just didn’t see an example of this.

Hi Egon,

Thanks for checking it out!

That actually works with some slight modifications. Here’s an example you can try with various combinations of flags (-n Egon, sub --help, -v --name=Bob, etc):

package main

import (
	"fmt"
	"github.com/ziuchkovski/writ"
	"os"
)

type Config struct {
	Help      bool   `flag:"help" description:"display this help message"`
	Verbosity int    `flag:"v, verbose" description:"display verbose output"`
	Name      string `option:"n, name" default:"Everyone" description:"the person to greet"`

	Sub struct {
		Help bool `flag:"help" description:"display sub command help message"`
	} `command:"sub"`
}

func main() {
	config := &Config{}
	cmd := writ.New("config", config)
	path, positional, err := cmd.Decode(os.Args[1:])
	if err != nil || config.Help {
		cmd.ExitHelp(err)
	}

	fmt.Printf("path: %s, positional: %s, config.Help: %t, config.Verbosity: %d, config.Name: %s, config.Sub.Help: %t\n", path, positional, config.Help, config.Verbosity, config.Name, config.Sub.Help)
}

Let me know if that makes sense or not. There’s a subcommand example in the README.md and godocs, but it might not be totally clear. I’ve been trying to improve the docs and examples, but documentation is a weakness for me. I’d love any and all feedback on doc improvements!

Bob

@bobziuchkovski With a set of options, are both the Go single dash) and GNU (double dash) forms supported? e.g. supplying -api-email=<email> instead of --api-email=<email> results in a run-time error.

I would also suggest running an internal compile() function early on and catching the panic so that invalid options/flags are easier to parse:

e.g.

# Should just keep this
panic: field type not valid as a flag -- did you mean to use "option" instead? (field APIKey)

goroutine 1 [running]:
github.com/ziuchkovski/writ.panicCommand(0x40f220, 0x4c, 0xc8200d18e0, 0x2, 0x2)
        /Users/matt/.go/src/github.com/ziuchkovski/writ/command.go:45 +0xad
github.com/ziuchkovski/writ.parseFlagField(0x395930, 0x6, 0x0, 0x0, 0x6560a8, 0x2a51a0, 0x402880, 0x35, 0x0, 0xc8200110c0, ...)
        /Users/matt/.go/src/github.com/ziuchkovski/writ/command.go:557 +0x9d8
github.com/ziuchkovski/writ.parseCommandSpec(0x3a2c10, 0xc, 0x28cd20, 0xc82008e280, 0xc82002c0b0, 0x1, 0x1, 0x0)
# <snip>

@elithrar Only the double-dash form is supported. GNU getopt_long allows aggregating single-dash options in two different ways, and the result would be ambiguous if writ also supported the Go single-dash. Examples: with a short-form -d option, getopt_long allows you do to -dfoo, which is equivalent to -d foo. Similarly, if -d and -e are flags (options without args), -de is allowed as a short-form for -d -e. Same thing for -v style verbosity flags: -vvv is short-form for -v -v -v. In the long-run, I felt it more important to support all of the getopt_long semantics so that applications consumed by end-users feel more “familiar” to them.

Thanks for the suggestion on the panic. Right now the API panics on any form of usage that is semantically incorrect since these are bugs introduced by the API consumer. The Command internals run a .validate() method before processing any arguments to ensure these types of incorrect usage bubble-up immediately during development. Anywhere a user could supply invalid input (Command.Decode), on the other hand, an error is returned instead, since you wouldn’t want your application bubbling up panics to the end-user.

Is there a way to change the output formatting for panics? I’m only aware of the full goroutine stack trace like the one you showed, but it might be that I’m not familiar with something you can tweak in the runtime package or similar. If you can filter the panic down on a per-panic basis, I think that would be perfectly fine. If there’s a dial you can turn but it affects the entire running application, then I wouldn’t want to turn it if it would surprise users of the library.

I’m thrilled you’re using the library. Please let me know if you hit anything else or if you have any other suggestions! Or for that matter, if my response to this suggestion doesn’t make sense. :smile:

Bob

2 Likes

@bobziuchkovski That makes sense.

RE: the panics - I wouldn’t panic and would instead just write an error out to os.Stderr and call os.Exit(1).

I have a question about explicitly providing a []*writ.Option value as per the below (pulled from the example):

    config := &Config{} // Has a config.HelpFlag field that calls writ.NewFlagDecoder(&config.help)
    cmd := &writ.Command{Name: "explicit"}
    cmd.Options = options // Where options is a []*writ.Option declared elsewhere
    _, _, err := cmd.Decode(os.Args[1:])
    if err != nil || config.HelpFlag {
        cmd.ExitHelp(err)
    }

How do you automatically generate the help text in this instance? When running the binary, no help output is written. I can (of course) use the friendlier writ.New function but this doesn’t allow me to pass in a []*writ.Option (which I find more explicit than struct tags).

@elithrar I’m not sure about exiting the app like that. Writ only panics if the API itself is misused. Since that’s a sign of a definite bug on the API consumer side, I think it’s good to give the full panic output for diagnostic purposes. The panic is completely unrelated to user input, so there’s no way for it to hide and pop-up later on the end-user side. Either the app runs when you’re developing (API is used correctly), or it panics immediately (API is misused and there’s a bug to fix in the app using writ).

For the help output, if you don’t use writ.New(), you’ll need to initialize some of the command’s Help fields: https://godoc.org/github.com/ziuchkovski/writ#Help. You’ll probably want to set Help.Usage and Help.OptionGroups. I struggled with whether to implicitly default these if they aren’t provided or not. I went with explicit since it’s possible someone would want to define a command without option output or one without the usage line output.

2 Likes

Just a quick follow-up on that. I added a note to the godocs for Command to make it a bit more clear. The assumption is basically that if you’re constructing Commands and Options directly, then you are in complete control. And if you’d like an example on how to add the Usage and OptionGroup, the relevant one would be here: https://github.com/ziuchkovski/writ/blob/master/example_explicit_test.go#L45-L47 . If you aren’t hiding any options, you can add an OptionGroup and set it to use the same []*Option that you’ve assigned the Command itself.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.