gitlab.com/zygoon/go-cmdr v1.9.0 has been released

I've just released go-cmdr version 1.9.0

This version brings in several small improvements and one deprecation I wanted to explain below.

Update: most of the new functionality is in: https://pkg.go.dev/gitlab.com/zygoon/go-cmdr@v1.9.0/format?tab=versions and https://pkg.go.dev/gitlab.com/zygoon/go-cmdr@v1.9.0/cmdtest?tab=versions

Prepared test invocation

The testcmd package, which simplifies testing command line applications, has gained support for replacing standard input stream. The existing cmdtest.Invoke function has been generalized to cmdtest.Prepare, which returns an Invocation object that can be further configured before being invoked with cmdtest.Invocation.RunPrepared. If you had been using the lower-level interface for providing replacement stdin, you should take advantage of this functionality to reduce the number of lines of boiler-plate code you have to maintain. You can look a sample unit tests that shows stdin replacement in action: https://gitlab.com/zygoon/go-cmdr/-/blob/main/cmdtest/cmdtest_test.go?ref_type=heads#L178 but the essence is just:

inv := cmdtest.Prepare(cmd)
if _, err := inv.Stdin.WriteString("potato"); err != nil {
    t.Fatal(err)
}
if err := inv.RunPrepared(); err != nil {
    t.Fatal(err)
}

Output formats

I've previously introduced OUTPUT_FORMAT= environment variable, which allows changing the format of the data printed to standard output. The go-cmdr library came with support for plain text, shell-variables and JSON output formats that application authors could integrate into their codebase.

The key operation was to select an output format and obtain a format-specific printer. In code it looks like this:

p, err := format.Negotiate(stdout, args, format.PlainTextSupport, format.ShellCompatSupport)
switch p := p.(type) {
   case *format.PlainPrinter:
        p.Println("Hello human")
   case *format.ShellPrinter:
        p.PrintSetVar("GREETING", "Hello automaton")
   default:
        panic("WAT?")
}

This worked fine but had some gotchas:

  • You have to type-switch on an any value. The type cases had to be exactly right *format.PlainPrinter works but format.PlainPrinter - which compiles silently - would just panic, hitting the WAT? default case.

  • Each format has a pair of types the format support indicator and the format printer. This is a necessary internal mechanic but it surely makes using formats more complex than they could be.

Version 1.9.0 deprecates format.Negotiate, introducing format.NegotiateCallback as the superior replacement.

Let's look at how our code sample looks like in the new scheme:

err := format.NegotiateCallback(ctx, stdout, args, 
    format.PlainText(func(ctx context.Context, p *format.PlainPrinter) error {
        p.Println("Hello human")
        return nil
    }),
    format.Shell(func(ctx context.Context, p *format.ShellPrinter) error {
        return p.PrintSetVar("GREETING", "Hello automaton")
    }),
))

As we can see the type switch and panic are gone. If we get any of the pointer types wrong the code no longer compiles. In retrospective I didn't realize how fragile the earlier type-switch was. I have decided to address this and introduce NegotiateCallback after hitting this in an untested branch of an application I was working on.

If the author of a library cannot use it correctly, it is a good sign the design needs improvement.

Old functionality remains in place, as I'm strongly against breaking backwards compatibility. You can keep using it, but consider migrating to the new symbol. I've marked is as deprecated to discourage new applications from using it by accident.


You'll only receive email when they publish something new.

More from Zygmunt Krynicki
All posts