Even if creating CLI applications is straighforward with Go, there are many libraries to help with that task, specially to handle subcommands and flags. Cobra is likely one of the more popular ones. It’s relatively simple to use and it’s easy to get something working very quickly.

A very basic example looks something like this:

rootCmd := &cobra.Command{
	Use:   "demo",
	Short: "demo shows how to pass a context.Context to a cobra command",
	Run:   rootFn,
}

if err := rootCmd.Execute(); err != nil {
	fmt.Println(err)
	os.Exit(1)
}

To get started, you define a command and then execute it. You define your CLI user interface by attaching additional commands and flags to this initial command.

The Run field in the cobra.Command type is a function that gets called when the corresponding command gets executed. The signature of that function must be:

func(cmd *Command, args []string)

When called, this function gets a pointer to the command and a slice with all the arguments passed on the command line.

Imagine you have a program like curl or wget, where you download something, possibly large, from the network. It would be useful to be able to pass a context.Context to this function, as you could use the signal package to catch the various interrupt signals from the OS and then use context.WithCancel to obtain a cancel function that can be called if the interrupt signal is received. Your program would interrupt the download and clean up any intermediate files.

For the above to work, it would be useful if the Run function received a context.Context and the Execute method received one, too. In other words, you would call something like:

if err := rootCmd.ExecuteWithContext(ctx); err != nil {
	fmt.Println(err)
	os.Exit(1)
}

and the type of the Run field (or RunWithContext) would be something like:

func(ctx context.Context, cmd *Command, args []string)

While current versions of cobra do not have support for this (but see this PR), it’s still possible to achieve this by adapting a function that does take a context.Context argument into one that does not, like this:

func addContext(func(context.Context, *cobra.Command, []string)) func(*cobra.Command, []string)

This function receives a function taking a context.Context argument and returns one that matches the signature that cobra needs. This is a common pattern in Go programs. The implementation uses a closure to capture the function argument, which is then used in the return function. The simplest version would be:

func addContext(fn func(context.Context, *cobra.Command, []string)) func(*cobra.Command, []string) {
	return func(cmd *cobra.Command, args []string) {
		ctx := context.Background()
		fn(ctx, cmd, args)
	}
}

Now you can do:

rootCmd := &cobra.Command{
	Use:   "demo",
	Short: "demo shows how to pass a context.Context to a cobra command",
	Run:   addContext(rootFnWithContext),
}

This gets you there, but it is less than ideal: the context is hardcoded within the adapter. Fixing this is simple:

func addContext(context.Context, func(context.Context, *cobra.Command, []string)) func(*cobra.Command, []string)

Now the command definition would look like this:

rootCmd := &cobra.Command{
	Use:   "demo",
	Short: "demo shows how to pass a context.Context to a cobra command",
	Run:   addContext(ctx, rootFnWithContext),
}

Now there’s a different problem: because of the way cobra is structured, it provides an incentive to define commands as package-scoped variables and to register commands using init functions (you know you should avoid them). While this is not a requirement, most documentation, examples and actual code out there will nudge you in that direction. This means that you don’t have a context to pass to addContext at the point where you define a command.

This problem is solved if you do not define your command as a package-scoped variable, and instead create it within a function that you can call after having created your context. Since implementing this might be awkward, specially with already existing code, you can look at another option.

Consider a type like the following:

type ctxAdder struct {
	ctx context.Context
}

Before going further, keep in mind that this goes against the recommended practice:

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it.

But then again, the standard library as a few instances of context.Context stored inside a struct type, for reasons quite similar to the case presented here. The authors have tried to argue that as long as you are doing something equivalent to the following you are fine:

type someStruct struct {
	ctx context.Context
}

// ...

s := someStruct{
	ctx: context.Background(),
}

someFunction(s, ...)

in other words, as along as you are passing the struct instance as you would be passing the context.Context one, it’s fine. The key concept here is that the context value should have a lifespan delimited by the associated request. Since CLI applications handle a single “request”, it should be OK to do what I’m describing here.

Back to the problem, with the ctxAdder type, you can do something like this:

func (c *ctxAdder) withContext(fn commandWithContext) func(cmd *cobra.Command, args []string) {
	return func(cmd *cobra.Command, args []string) {
		fn(c.ctx, cmd, args)
	}
}

and then:

rootCmd := &cobra.Command{
	Use:   "demo",
	Short: "demo shows how to pass a context.Context to a cobra command",
	Run:   adder.withContext(rootFnWithContext),
}

now in addition to the target function, the closure also captures the ctxAdder instance (adder in the above code, c in the implementation of withContext).

Now it’s possible to set the context just before running rootCmd.Execute:

adder.setContext(ctx)

if err := rootCmd.Execute(); err != nil {
	fmt.Println(err)
	os.Exit(1)
}

With this implementation ctx does not need to be a package-level variable, but, depending on the structure of your program, adder probably does. The difference is that you can delay creating the context as much as it makes sense in your program.

A full implementation can be found here. The code follows the common structure of programs using cobra in order to show that the solution is workable without introducing too many modifications to already existing code. Both cmd1 and cmd2 are defined using the same adapted Run function that takes a context as argument, with the difference that cmd1 receives a context that was defined “too early” and cmd2 receives a context that was defined in the main function of the program and is integrated with a signal handler. Try using both commands and pressing ctrl-C to see what happens.

Enjoy!