
execx is an ergonomic, fluent wrapper around Go’s `os/exec` package.
What execx is
execx is a small, explicit wrapper around os/exec. It keeps the exec.Cmd model but adds fluent construction and consistent result handling.
There is no shell interpolation. Arguments, environment, and I/O are set directly, and nothing runs until you call Run, Output, or Start.
Installation
go get github.com/goforj/execxQuick Start
out, _ := execx.Command("echo", "hello").OutputTrimmed()
fmt.Println(out)
// #string helloOn Windows, use cmd /c echo hello or powershell -Command "echo hello" for shell built-ins.
Basic usage
Build a command and run it:
cmd := execx.Command("echo").Arg("hello")
res, _ := cmd.Run()
fmt.Print(res.Stdout)
// helloArguments are appended deterministically and never shell-expanded.
Output handling
Use Output variants when you only need stdout:
out, _ := execx.Command("echo", "hello").OutputTrimmed()
fmt.Println(out)
// #string helloOutput, OutputBytes, OutputTrimmed, and CombinedOutput differ only in how they return data.
Pipelining
Pipelines run on all platforms; command availability is OS-specific.
out, _ := execx.Command("printf", "go").
Pipe("tr", "a-z", "A-Z").
OutputTrimmed()
fmt.Println(out)
// #string GOOn Windows, use cmd /c or powershell -Command for shell built-ins.
PipeStrict (default) stops at the first failing stage and returns that error.PipeBestEffort runs all stages, returns the last stage output, and surfaces the first error if any stage failed.
Context & cancellation
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, _ := execx.Command("go", "env", "GOOS").WithContext(ctx).Run()
fmt.Println(res.ExitCode == 0)
// #bool trueEnvironment & I/O control
Environment is explicit and deterministic:
cmd := execx.Command("echo", "hello").Env("MODE=prod")
fmt.Println(strings.Contains(strings.Join(cmd.EnvList(), ","), "MODE=prod"))
// #bool trueStandard input is opt-in:
out, _ := execx.Command("cat").
StdinString("hi").
OutputTrimmed()
fmt.Println(out)
// #string hiAdvanced features
For process control, use Start with the Process helpers:
proc := execx.Command("go", "env", "GOOS").Start()
res, _ := proc.Wait()
fmt.Println(res.ExitCode == 0)
// #bool trueSignals, timeouts, and OS controls are documented in the API section below.
ShadowPrint is available for emitting the command line before and after execution.
Kitchen Sink Chaining Example
// Run executes the command and returns the result and any error.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
res, err := execx.
Command("printf", "hello\nworld\n").
Pipe("tr", "a-z", "A-Z").
Env("MODE=demo").
WithContext(ctx).
OnStdout(func(line string) {
fmt.Println("OUT:", line)
}).
OnStderr(func(line string) {
fmt.Println("ERR:", line)
}).
Run()
if !res.OK() {
log.Fatalf("command failed: %v", err)
}
fmt.Printf("Stdout: %q\n", res.Stdout)
fmt.Printf("Stderr: %q\n", res.Stderr)
fmt.Printf("ExitCode: %d\n", res.ExitCode)
fmt.Printf("Error: %v\n", res.Err)
fmt.Printf("Duration: %v\n", res.Duration)
// OUT: HELLO
// OUT: WORLD
// Stdout: "HELLO\nWORLD\n"
// Stderr: ""
// ExitCode: 0
// Error: <nil>
// Duration: 10.123456msError handling model
execx returns two error surfaces:
err(fromRun,Output,CombinedOutput,Wait, etc) only reports execution failures:- start failures (binary not found, not executable, OS start error)
- context cancellations or timeouts (
WithContext,WithTimeout,WithDeadline) - pipeline failures based on
PipeStrict/PipeBestEffort
Result.Errmirrorserrfor convenience; it is not for exit status.
Exit status is always reported via Result.ExitCode, even on non-zero exits. A non-zero exit does not automatically produce err.
Use err when you want to handle execution failures, and check Result.ExitCode (or Result.OK() / Result.IsExitCode) when you care about command success.
Non-goals and design principles
Design principles:
- Explicit over implicit
- No shell interpolation
- Composable, deterministic behavior
Non-goals:
- Shell scripting replacement
- Command parsing or glob expansion
- Task runners or build systems
- Automatic retries or heuristics
All public APIs are covered by runnable examples under ./examples, and the test suite executes them to keep docs and behavior in sync.
API Index
| Group | Functions |
|---|---|
| Arguments | Arg |
| Construction | Command |
| Context | WithContext WithDeadline WithTimeout |
| Debugging | Args ShellEscaped String |
| Decoding | Decode DecodeJSON DecodeWith DecodeYAML FromCombined FromStderr FromStdout Into Trim |
| Environment | Env EnvAppend EnvInherit EnvList EnvOnly |
| Errors | Error Unwrap |
| Execution | CombinedOutput Output OutputBytes OutputTrimmed Run Start OnExecCmd |
| Input | StdinBytes StdinFile StdinReader StdinString |
| OS Controls | CreationFlags HideWindow Pdeathsig Setpgid Setsid |
| Pipelining | Pipe PipeBestEffort PipeStrict PipelineResults |
| Process | GracefulShutdown Interrupt KillAfter Send Terminate Wait |
| Results | IsExitCode IsSignal OK |
| Shadow Print | ShadowOff ShadowOn ShadowPrint WithFormatter WithMask WithPrefix |
| Streaming | OnStderr OnStdout StderrWriter StdoutWriter WithPTY |
| WorkingDir | Dir |
Arguments
Arg
Arg appends arguments to the command.
cmd := execx.Command("printf").Arg("hello")
out, _ := cmd.Output()
fmt.Print(out)
// helloConstruction
Command
Command constructs a new command without executing it.
cmd := execx.Command("printf", "hello")
out, _ := cmd.Output()
fmt.Print(out)
// helloContext
WithContext
WithContext binds the command to a context.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, _ := execx.Command("go", "env", "GOOS").WithContext(ctx).Run()
fmt.Println(res.ExitCode == 0)
// #bool trueWithDeadline
WithDeadline binds the command to a deadline.
res, _ := execx.Command("go", "env", "GOOS").WithDeadline(time.Now().Add(2 * time.Second)).Run()
fmt.Println(res.ExitCode == 0)
// #bool trueWithTimeout
WithTimeout binds the command to a timeout.
res, _ := execx.Command("go", "env", "GOOS").WithTimeout(2 * time.Second).Run()
fmt.Println(res.ExitCode == 0)
// #bool trueDebugging
Args
Args returns the argv slice used for execution.
cmd := execx.Command("go", "env", "GOOS")
fmt.Println(strings.Join(cmd.Args(), " "))
// #string go env GOOSShellEscaped
ShellEscaped returns a shell-escaped string for logging only.
cmd := execx.Command("echo", "hello world", "it's")
fmt.Println(cmd.ShellEscaped())
// #string echo 'hello world' "it's"String
String returns a human-readable representation of the command.
cmd := execx.Command("echo", "hello world", "it's")
fmt.Println(cmd.String())
// #string echo "hello world" it'sDecoding
Decode
Decode configures a custom decoder for this command. Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
type payload struct {
Name string
}
decoder := execx.DecoderFunc(func(data []byte, dst any) error {
out, ok := dst.(*payload)
if !ok {
return fmt.Errorf("expected *payload")
}
_, val, ok := strings.Cut(string(data), "=")
if !ok {
return fmt.Errorf("invalid payload")
}
out.Name = val
return nil
})
var out payload
_ = execx.Command("printf", "name=gopher").
Decode(decoder).
Into(&out)
fmt.Println(out.Name)
// #string gopherDecodeJSON
DecodeJSON configures JSON decoding for this command. Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
type payload struct {
Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", `{"name":"gopher"}`).
DecodeJSON().
Into(&out)
fmt.Println(out.Name)
// #string gopherDecodeWith
DecodeWith executes the command and decodes stdout into dst.
type payload struct {
Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", `{"name":"gopher"}`).
DecodeWith(&out, execx.DecoderFunc(json.Unmarshal))
fmt.Println(out.Name)
// #string gopherDecodeYAML
DecodeYAML configures YAML decoding for this command. Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
type payload struct {
Name string `yaml:"name"`
}
var out payload
_ = execx.Command("printf", "name: gopher").
DecodeYAML().
Into(&out)
fmt.Println(out.Name)
// #string gopherFromCombined
FromCombined decodes from combined stdout+stderr.
type payload struct {
Name string `json:"name"`
}
var out payload
_ = execx.Command("sh", "-c", `printf '{"name":"gopher"}'`).
DecodeJSON().
FromCombined().
Into(&out)
fmt.Println(out.Name)
// #string gopherFromStderr
FromStderr decodes from stderr.
type payload struct {
Name string `json:"name"`
}
var out payload
_ = execx.Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
DecodeJSON().
FromStderr().
Into(&out)
fmt.Println(out.Name)
// #string gopherFromStdout
FromStdout decodes from stdout (default).
type payload struct {
Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", `{"name":"gopher"}`).
DecodeJSON().
FromStdout().
Into(&out)
fmt.Println(out.Name)
// #string gopherInto
Into executes the command and decodes into dst.
type payload struct {
Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", `{"name":"gopher"}`).
DecodeJSON().
Into(&out)
fmt.Println(out.Name)
// #string gopherTrim
Trim trims whitespace before decoding.
type payload struct {
Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", " {\"name\":\"gopher\"} ").
DecodeJSON().
Trim().
Into(&out)
fmt.Println(out.Name)
// #string gopherEnvironment
Env
Env adds environment variables to the command.
cmd := execx.Command("go", "env", "GOOS").Env("MODE=prod")
fmt.Println(strings.Contains(strings.Join(cmd.EnvList(), ","), "MODE=prod"))
// #bool trueEnvAppend
EnvAppend merges variables into the inherited environment.
cmd := execx.Command("go", "env", "GOOS").EnvAppend(map[string]string{"A": "1"})
fmt.Println(strings.Contains(strings.Join(cmd.EnvList(), ","), "A=1"))
// #bool trueEnvInherit
EnvInherit restores default environment inheritance.
cmd := execx.Command("go", "env", "GOOS").EnvInherit()
fmt.Println(len(cmd.EnvList()) > 0)
// #bool trueEnvList
EnvList returns the environment list for execution.
cmd := execx.Command("go", "env", "GOOS").EnvOnly(map[string]string{"A": "1"})
fmt.Println(strings.Join(cmd.EnvList(), ","))
// #string A=1EnvOnly
EnvOnly ignores the parent environment.
cmd := execx.Command("go", "env", "GOOS").EnvOnly(map[string]string{"A": "1"})
fmt.Println(strings.Join(cmd.EnvList(), ","))
// #string A=1Errors
Error
Error returns the wrapped error message when available.
err := execx.ErrExec{Err: fmt.Errorf("boom")}
fmt.Println(err.Error())
// #string boomUnwrap
Unwrap exposes the underlying error.
err := execx.ErrExec{Err: fmt.Errorf("boom")}
fmt.Println(err.Unwrap() != nil)
// #bool trueExecution
CombinedOutput
CombinedOutput executes the command and returns stdout+stderr and any error.
out, err := execx.Command("go", "env", "-badflag").CombinedOutput()
fmt.Print(out)
fmt.Println(err == nil)
// flag provided but not defined: -badflag
// usage: go env [-json] [-changed] [-u] [-w] [var ...]
// Run 'go help env' for details.
// falseOutput
Output executes the command and returns stdout and any error.
out, _ := execx.Command("printf", "hello").Output()
fmt.Print(out)
// helloOutputBytes
OutputBytes executes the command and returns stdout bytes and any error.
out, _ := execx.Command("printf", "hello").OutputBytes()
fmt.Println(string(out))
// #string helloOutputTrimmed
OutputTrimmed executes the command and returns trimmed stdout and any error.
out, _ := execx.Command("printf", "hello\n").OutputTrimmed()
fmt.Println(out)
// #string helloRun
Run executes the command and returns the result and any error.
res, _ := execx.Command("go", "env", "GOOS").Run()
fmt.Println(res.ExitCode == 0)
// #bool trueStart
Start executes the command asynchronously.
proc := execx.Command("go", "env", "GOOS").Start()
res, _ := proc.Wait()
fmt.Println(res.ExitCode == 0)
// #bool trueOnExecCmd
OnExecCmd registers a callback to mutate the underlying exec.Cmd before start.
_, _ = execx.Command("printf", "hi").
OnExecCmd(func(cmd *exec.Cmd) {
cmd.Env = append(cmd.Env, "EXAMPLE=1")
}).
Run()Input
StdinBytes
StdinBytes sets stdin from bytes.
out, _ := execx.Command("cat").
StdinBytes([]byte("hi")).
Output()
fmt.Println(out)
// #string hiStdinFile
StdinFile sets stdin from a file.
file, _ := os.CreateTemp("", "execx-stdin")
_, _ = file.WriteString("hi")
_, _ = file.Seek(0, 0)
out, _ := execx.Command("cat").
StdinFile(file).
Output()
fmt.Println(out)
// #string hiStdinReader
StdinReader sets stdin from an io.Reader.
out, _ := execx.Command("cat").
StdinReader(strings.NewReader("hi")).
Output()
fmt.Println(out)
// #string hiStdinString
StdinString sets stdin from a string.
out, _ := execx.Command("cat").
StdinString("hi").
Output()
fmt.Println(out)
// #string hiOS Controls
CreationFlags
CreationFlags is a no-op on non-Windows platforms; on Windows it sets process creation flags.
out, _ := execx.Command("printf", "ok").CreationFlags(execx.CreateNewProcessGroup).Output()
fmt.Print(out)
// okHideWindow
HideWindow is a no-op on non-Windows platforms; on Windows it hides console windows.
out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
fmt.Print(out)
// okPdeathsig
Pdeathsig is a no-op on non-Linux platforms; on Linux it signals the child when the parent exits.
out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
fmt.Print(out)
// okSetpgid
Setpgid places the child in a new process group for group signals.
out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
fmt.Print(out)
// okSetsid
Setsid starts the child in a new session, detaching it from the terminal.
out, _ := execx.Command("printf", "ok").Setsid(true).Output()
fmt.Print(out)
// okPipelining
Pipe
Pipe appends a new command to the pipeline. Pipelines run on all platforms.
out, _ := execx.Command("printf", "go").
Pipe("tr", "a-z", "A-Z").
OutputTrimmed()
fmt.Println(out)
// #string GOPipeBestEffort
PipeBestEffort sets best-effort pipeline semantics (run all stages, surface the first error).
res, _ := execx.Command("false").
Pipe("printf", "ok").
PipeBestEffort().
Run()
fmt.Print(res.Stdout)
// okPipeStrict
PipeStrict sets strict pipeline semantics (stop on first failure).
res, _ := execx.Command("false").
Pipe("printf", "ok").
PipeStrict().
Run()
fmt.Println(res.ExitCode != 0)
// #bool truePipelineResults
PipelineResults executes the command and returns per-stage results and any error.
results, _ := execx.Command("printf", "go").
Pipe("tr", "a-z", "A-Z").
PipelineResults()
fmt.Printf("%+v", results)
// [
// {Stdout:go Stderr: ExitCode:0 Err:<nil> Duration:6.367208ms signal:<nil>}
// {Stdout:GO Stderr: ExitCode:0 Err:<nil> Duration:4.976291ms signal:<nil>}
// ]Process
GracefulShutdown
GracefulShutdown sends a signal and escalates to kill after the timeout.
proc := execx.Command("sleep", "2").Start()
_ = proc.GracefulShutdown(os.Interrupt, 100*time.Millisecond)
res, _ := proc.Wait()
fmt.Println(res.IsSignal(os.Interrupt))
// #bool trueInterrupt
Interrupt sends an interrupt signal to the process.
proc := execx.Command("sleep", "2").Start()
_ = proc.Interrupt()
res, _ := proc.Wait()
fmt.Printf("%+v", res)
// {Stdout: Stderr: ExitCode:-1 Err:<nil> Duration:75.987ms signal:interrupt}KillAfter
KillAfter terminates the process after the given duration.
proc := execx.Command("sleep", "2").Start()
proc.KillAfter(100 * time.Millisecond)
res, _ := proc.Wait()
fmt.Printf("%+v", res)
// {Stdout: Stderr: ExitCode:-1 Err:<nil> Duration:100.456ms signal:killed}Send
Send sends a signal to the process.
proc := execx.Command("sleep", "2").Start()
_ = proc.Send(os.Interrupt)
res, _ := proc.Wait()
fmt.Printf("%+v", res)
// {Stdout: Stderr: ExitCode:-1 Err:<nil> Duration:80.123ms signal:interrupt}Terminate
Terminate kills the process immediately.
proc := execx.Command("sleep", "2").Start()
_ = proc.Terminate()
res, _ := proc.Wait()
fmt.Printf("%+v", res)
// {Stdout: Stderr: ExitCode:-1 Err:<nil> Duration:70.654ms signal:killed}Wait
Wait waits for the command to complete and returns the result and any error.
proc := execx.Command("go", "env", "GOOS").Start()
res, _ := proc.Wait()
fmt.Printf("%+v", res)
// {Stdout:darwin
// Stderr: ExitCode:0 Err:<nil> Duration:1.234ms signal:<nil>}Results
IsExitCode
IsExitCode reports whether the exit code matches.
res, _ := execx.Command("go", "env", "GOOS").Run()
fmt.Println(res.IsExitCode(0))
// #bool trueIsSignal
IsSignal reports whether the command terminated due to a signal.
res, _ := execx.Command("go", "env", "GOOS").Run()
fmt.Println(res.IsSignal(os.Interrupt))
// falseOK
OK reports whether the command exited cleanly without errors.
res, _ := execx.Command("go", "env", "GOOS").Run()
fmt.Println(res.OK())
// #bool trueShadow Print
ShadowOff
ShadowOff disables shadow printing for this command chain, preserving configuration.
_, _ = execx.Command("printf", "hi").ShadowPrint().ShadowOff().Run()ShadowOn
ShadowOn enables shadow printing using the previously configured options.
cmd := execx.Command("printf", "hi").
ShadowPrint(execx.WithPrefix("run"))
cmd.ShadowOff()
_, _ = cmd.ShadowOn().Run()
// run > printf hi
// run > printf hi (1ms)ShadowPrint
ShadowPrint configures shadow printing for this command chain.
Example: shadow print
_, _ = execx.Command("bash", "-c", `echo "hello world"`).
ShadowPrint().
OnStdout(func(line string) { fmt.Println(line) }).
Run()
// execx > bash -c 'echo "hello world"'
//
// hello world
//
// execx > bash -c 'echo "hello world"' (1ms)Example: shadow print options
mask := func(cmd string) string {
return strings.ReplaceAll(cmd, "token", "***")
}
formatter := func(ev execx.ShadowEvent) string {
return fmt.Sprintf("shadow: %s %s", ev.Phase, ev.Command)
}
_, _ = execx.Command("bash", "-c", `echo "hello world"`).
ShadowPrint(
execx.WithPrefix("execx"),
execx.WithMask(mask),
execx.WithFormatter(formatter),
).
OnStdout(func(line string) { fmt.Println(line) }).
Run()
// shadow: before bash -c 'echo "hello world"'
// hello world
// shadow: after bash -c 'echo "hello world"'WithFormatter
WithFormatter sets a formatter for ShadowPrint output.
formatter := func(ev execx.ShadowEvent) string {
return fmt.Sprintf("shadow: %s %s", ev.Phase, ev.Command)
}
_, _ = execx.Command("printf", "hi").ShadowPrint(execx.WithFormatter(formatter)).Run()
// shadow: before printf hi
// shadow: after printf hiWithMask
WithMask applies a masker to the shadow-printed command string.
mask := func(cmd string) string {
return strings.ReplaceAll(cmd, "secret", "***")
}
_, _ = execx.Command("printf", "secret").ShadowPrint(execx.WithMask(mask)).Run()
// execx > printf ***
// execx > printf *** (1ms)WithPrefix
WithPrefix sets the shadow print prefix.
_, _ = execx.Command("printf", "hi").ShadowPrint(execx.WithPrefix("run")).Run()
// run > printf hi
// run > printf hi (1ms)Streaming
OnStderr
OnStderr registers a line callback for stderr.
_, err := execx.Command("go", "env", "-badflag").
OnStderr(func(line string) {
fmt.Println(line)
}).
Run()
fmt.Println(err == nil)
// flag provided but not defined: -badflag
// usage: go env [-json] [-changed] [-u] [-w] [var ...]
// Run 'go help env' for details.
// falseOnStdout
OnStdout registers a line callback for stdout.
_, _ = execx.Command("printf", "hi\n").
OnStdout(func(line string) { fmt.Println(line) }).
Run()
// hiStderrWriter
StderrWriter sets a raw writer for stderr.
When the writer is a terminal and no line callbacks or combined output are enabled, execx passes stderr through directly and does not buffer it for results.
var out strings.Builder
_, err := execx.Command("go", "env", "-badflag").
StderrWriter(&out).
Run()
fmt.Print(out.String())
fmt.Println(err == nil)
// flag provided but not defined: -badflag
// usage: go env [-json] [-changed] [-u] [-w] [var ...]
// Run 'go help env' for details.
// falseStdoutWriter
StdoutWriter sets a raw writer for stdout.
When the writer is a terminal and no line callbacks or combined output are enabled, execx passes stdout through directly and does not buffer it for results.
var out strings.Builder
_, _ = execx.Command("printf", "hello").
StdoutWriter(&out).
Run()
fmt.Print(out.String())
// helloWithPTY
WithPTY attaches stdout/stderr to a pseudo-terminal.
Output is combined; OnStdout and OnStderr receive the same lines, and Result.Stderr remains empty. Platforms without PTY support return an error when the command runs.
_, _ = execx.Command("printf", "hi").
WithPTY().
OnStdout(func(line string) { fmt.Println(line) }).
Run()
// hiWorkingDir
Dir
Dir sets the working directory.
dir := os.TempDir()
out, _ := execx.Command("pwd").
Dir(dir).
OutputTrimmed()
fmt.Println(out == dir)
// #bool true