Facebook Linkedin Twitter
Posted Sat Dec 11, 2021 •  Reading time: 4 minutes

Gotchas when running failing commands in the Go os/exec package

One of the nicest packages in the Go standard library is os/exec, that provides a very simple and concise API to execute external commands from a Go program:

err := exec.Command("kubectl", "get", "foos").Run()
if err != nil {
	log.Fatal(err)
}

If your command succeeds, err will be nil and you can continue safely, but if it fails, you can inspect additional information about the error. According to the documentation, you will find that for the case when the external program exits with a non-zero status, the return error is of type exec.ExitError, defined as follows:

type ExitError struct {
	*os.ProcessState
	Stderr []byte
}

This is great! However, if you want to read the contents of Stderr, you need to carefully read the documentation about it:

Stderr holds a subset of the standard error output from the Cmd.Output method if standard error was not otherwise being collected.

If the error output is long, Stderr may contain only a prefix and suffix of the output, with the middle replaced with text about the number of omitted bytes.

Stderr is provided for debugging, for inclusion in error messages. Users with other needs should redirect Cmd.Stderr as needed.

This property will have contents only if you are calling *Cmd.Output, not if you use *Cmd.Run or *Cmd.Wait. Not only that, the contents can be truncated! I find this to be counterintuitive and easy to forget, something I know for a fact because I’ve been bitten by this more than once, as at least half of the time I use this package I only care about the program succeeding or not, not about its output.

What are the alternatives? The easiest is to manually assign an io.Writer to *Cmd.Stderr, like this:

var stderr bytes.Buffer
cmd := exec.Command("kubelet", "get", "foos")
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
  log.Println(err)
  os.Stderr.Write(stderr.Bytes())
}

This works, but it gets too repetitive and verbose if you need to execute multiple commands. Also, because the stderr output isn’t part of the error, if you simply return the error the caller won’t have access to that information. You could of course replace the code above with the following:

var stderr bytes.Buffer
cmd := exec.Command("kubelet", "get", "foos")
cmd.Stderr = &stderr
err := cmd.Run()
var exErr *exec.ExitError
if errors.As(err, &exErr) {
  exErr.Stderr = stderr.Bytes()
  return exErr
}

This pattern works, but imagine you now need to execute two commands using this pattern; quite a nightmare. You can try to DRY and write a helper function to abstract all this, and it’s definitely something I recommend. But if use both *Cmd.Run and *Cmd.Wait then you need to define an almost exact copy of the wrapper for each function, or write a really ugly wrapper… or keep reading and I promise to give you better alternative. But I digress.

At this point I hope you’ve come to the realization that having to write all this code isn’t the real issue, the real issue is that you cannot deal with *exec.ExitError in a consistent manner, and that worries me. By using this standard library package, you need to be aware if you’re calling *Cmd.Output or *Cmd.Run or *Cmd.Wait and then if you have assigned an io.Writer to *Cmd.Stderr or not, et cetera. Trust me, this can be a real pain to deal with.

After a while of dealing with these issues, I started to think would I like to see in the os/exec package, and found that I wouldn’t change anything except that it always captures standard error. So I decided to make my own library (spoilers: head to github.com/inkel/exex) that deals with this in a simple and elegant solution.

With this package now you can have the following program and always having your stderr contents if it fails:

package main

import (
	"errors"
	"fmt"
	"os/exec"

	"github.com/inkel/exex"
)

func main() {
	err := exex.Command("kubectl", "get", "foobar").Run()

	var exErr *exec.ExitError

	if !errors.As(err, &exErr) {
		panic("this shouldn't happen!")
	}

	fmt.Printf("Failed with: %s\n", exErr.Stderr)
}

You will always get the contents of standard error! Whereas if you were to create the command using exec.Command then exErr.Stderr wouldn’t have any contents.

I can imagine that now you’re saying this is all nice and shiny, but I still need to import os/exec to cast to exec.ExitError! and let me tell you that you don’t really need that: this package also includes a type alias for ExitError so you only need to import github.com/inkel/exex; then you can do the following:

package main

import (
	"errors"
	"fmt"

	"github.com/inkel/exex"
)

func main() {
	err := exex.Command("kubectl", "get", "foobar").Run()

	var exErr *exex.ExitError

	if !errors.As(err, &exErr) {
		panic("this shouldn't happen!")
	}

	fmt.Printf("Failed with: %q\n", exErr.Stderr)
}

Neat, huh?

Working on this library taught me lots about Go as a language, as an ecosystem, and as a philosophy. I encourage you to go ahead and follow the commits log to see how the library evolved, it was a great learning experience for me and I think it will be the same for you.

Thanks for reading!