Go Basics: Pass Subprocess Arguments Without a Shell
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 1Run 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.goObserved output:
Literal arguments:
tea & cake
$(echo not-run)
Child exit code: 1Read 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_dirIf the directory is not empty, inspect its remaining contents before removing anything else.
Acknowledgements
This post was created with assistance from AI.