goforj/execx
execx is an ergonomic, fluent wrapper around Go’s os/exec package that makes running external commands simple, expressive, and safe.
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