Skip to content
Go Basics: Pass Subprocess Arguments Without a Shell

Go Basics: Pass Subprocess Arguments Without a Shell

October 31, 2026

Arguments containing shell punctuation should stay literal when launching a tool. Use exec.Command with separate arguments and handle the child’s exit status explicitly.

Prepare a scratch directory

Tested on Linux amd64 with Go 1.26.10. Install Go and make go available on PATH. You also need Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. The example uses only the standard library and needs no module or external dependency.

Keep the same terminal open. Stop if setup fails, before saving the source:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Run the example

Save this as exec-arguments-demo.go:

package main

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

func main() {
	output, err := exec.Command("/usr/bin/printf", "%s\n", "tea & cake", "$(echo not-run)").Output()
	if err != nil {
		panic(err)
	}
	fmt.Printf("Literal arguments:\n%s", output)
	err = exec.Command("/usr/bin/false").Run()
	var exitErr *exec.ExitError
	if !errors.As(err, &exitErr) {
		panic(fmt.Sprintf("unexpected result: %v", err))
	}
	fmt.Println("Child exit code:", exitErr.ExitCode())
}

Run it:

go run exec-arguments-demo.go

Observed output:

Literal arguments:
tea & cake
$(echo not-run)
Child exit code: 1

Read the result

exec.Command launches the executable directly; it does not interpret ampersands or command substitutions. The fixed printf format ensures the supplied values are data. A nonzero child exit becomes an ExitError, distinct from failures such as an executable that cannot be started.

Apply it carefully

This Linux example requires /usr/bin/printf and /usr/bin/false. Avoid adding sh -c around untrusted arguments. Direct execution still requires validation of tool-specific options and resource limits. For long-running commands, use CommandContext with a deadline and plan how to handle descendants and output volume.

The official Go reference documents the behavior used here.

Clean up

Remove the saved source, return to the starting directory, and remove the empty practice directory. go run does not leave an executable here; Go manages its build cache separately.

rm -- exec-arguments-demo.go
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If the directory is not empty, inspect its remaining contents before removing anything else.

Acknowledgements

This post was created with assistance from AI.

Last updated on