Skip to content
Support this libraryStar on GitHub

execx logo

execx is an ergonomic, fluent wrapper around Go’s `os/exec` package.

Go ReferenceLicense: MITGo TestGo versionLatest tagTests

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.

The core contract is deliberately narrow:

  • a non-zero child exit is data in Result.ExitCode, not a Go error;
  • start, context, and I/O failures are returned as errors and mirrored in Result.Err;
  • a Cmd is mutable and must not be configured or executed concurrently;
  • output callbacks and caller-provided writers are serialized within one pipeline;
  • strings shown by String, ShellEscaped, and ShadowPrint are for display only and are never executed by a shell.

Installation ​

execx requires Go 1.27 or newer. Projects on older Go releases can pin the v1.1 release line.

bash
go get github.com/goforj/execx

Quick Start ​

go
out, _ := execx.Command("echo", "hello").OutputTrimmed()
fmt.Println(out)
// #string hello

On Windows, use cmd /c echo hello or powershell -Command "echo hello" for shell built-ins.

Basic usage ​

Build a command and run it:

go
cmd := execx.Command("echo").Arg("hello")
res, _ := cmd.Run()
fmt.Print(res.Stdout)
// hello

Arguments are appended deterministically and never shell-expanded.

Output handling ​

Use Output variants when you only need stdout:

go
out, _ := execx.Command("echo", "hello").OutputTrimmed()
fmt.Println(out)
// #string hello

Output, OutputBytes, OutputTrimmed, and CombinedOutput differ only in how they return data.

Pipelining ​

Pipelines run on all platforms; command availability is OS-specific.

go
out, _ := execx.Command("printf", "go").
	Pipe("tr", "a-z", "A-Z").
	OutputTrimmed()
fmt.Println(out)
// #string GO

On Windows, use cmd /c or powershell -Command for shell built-ins.

Every stage is started concurrently. PipeStrict (default) selects the first stage with an execution error or non-zero exit as the primary result. PipeBestEffort selects the last stage and surfaces the first execution error. Neither mode turns a non-zero exit into a Go error.

Configuration is stage-local. Configure a stage before calling Pipe, then configure the returned stage separately:

go
first := execx.Command("producer").Env("ROLE=producer")
second := first.Pipe("consumer").Env("ROLE=consumer")
res, err := second.Run()

PipeStrict, PipeBestEffort, WithPTY, and shadow-print settings are chain-wide.

Context & cancellation ​

go
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 true

Contexts are stage-local in pipelines. Replacing WithTimeout or WithDeadline keeps the context supplied through WithContext as the parent, including an already-canceled parent.

Environment & I/O control ​

Environment is explicit and deterministic:

go
cmd := execx.Command("echo", "hello").Env("MODE=prod")
fmt.Println(strings.Contains(strings.Join(cmd.EnvList(), ","), "MODE=prod"))
// #bool true

Standard input is opt-in:

go
out, _ := execx.Command("cat").
	StdinString("hi").
	OutputTrimmed()
fmt.Println(out)
// #string hi

StdinBytes copies its input. OnStdout and OnStderr receive complete lines, including a final line without a trailing newline. A failure from StdoutWriter or StderrWriter is returned as ErrExec; bytes received before that failure remain available in Result.

Advanced features ​

For process control, use Start with the Process helpers:

go
proc := execx.Command("go", "env", "GOOS").Start()
res, _ := proc.Wait()
fmt.Println(res.ExitCode == 0)
// #bool true

Signals, 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 ​

go
// 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.123456ms

Error handling model ​

execx returns two error surfaces:

  1. err (from Run, 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)
    • output capture, callback writer, and caller-provided writer failures
    • pipeline failures based on PipeStrict / PipeBestEffort
  2. Result.Err mirrors err for 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.

Security and logging ​

Command(name, args...) invokes the executable directly. It does not parse shell syntax, expand variables, glob paths, or interpret redirections. If you explicitly run a shell such as sh -c or bash -c, the command string becomes subject to that shell's rules and must be treated as untrusted input.

ShadowPrint can expose arguments. Use WithMask for secrets. Custom formatters receive both masked ShadowEvent.Command and unmasked ShadowEvent.RawCommand; avoid logging the raw value.

Compatibility notes ​

The v1 API and its non-zero-exit contract remain unchanged. This quality pass tightens four cases that were previously lossy or unsafe:

  • final unterminated output is now delivered to line callbacks;
  • caller-provided writer and PTY copy failures now return ErrExec;
  • StdinBytes no longer observes later mutations of the caller's slice;
  • replacement timeouts and deadlines no longer detach from their original parent context.

Code that intentionally depended on one of those edge behaviors should update before adopting the next release. No exported signature changed.

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 ​

Arguments ​

Arg ​

Arg appends arguments to the command.

go
cmd := execx.Command("printf").Arg("hello")
out, _ := cmd.Output()
fmt.Print(out)
// hello

Construction ​

Command ​

Command constructs a new command without executing it.

go
cmd := execx.Command("printf", "hello")
out, _ := cmd.Output()
fmt.Print(out)
// hello

Context ​

WithContext ​

WithContext binds the command to a context. A nil context is normalized to context.Background when execution begins.

go
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 true

WithDeadline ​

WithDeadline binds the command to a deadline. Replacing a deadline retains the context previously supplied through WithContext as its parent.

go
res, _ := execx.Command("go", "env", "GOOS").WithDeadline(time.Now().Add(2 * time.Second)).Run()
fmt.Println(res.ExitCode == 0)
// #bool true

WithTimeout ​

WithTimeout binds the command to a timeout. Replacing a timeout retains the context previously supplied through WithContext as its parent.

go
res, _ := execx.Command("go", "env", "GOOS").WithTimeout(2 * time.Second).Run()
fmt.Println(res.ExitCode == 0)
// #bool true

Debugging ​

Args ​

Args returns the argv slice used for execution.

go
cmd := execx.Command("go", "env", "GOOS")
fmt.Println(strings.Join(cmd.Args(), " "))
// #string go env GOOS

ShellEscaped ​

ShellEscaped returns a shell-escaped string for logging only.

go
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.

go
cmd := execx.Command("echo", "hello world", "it's")
fmt.Println(cmd.String())
// #string echo "hello world" it's

Decoding ​

As ​

As executes the command and decodes its selected output into T.

go
type payload struct {
	Name string `json:"name"`
}
out, err := execx.Command("printf", `{"name":"gopher"}`).
	DecodeJSON().
	As[payload]()
fmt.Println(err == nil, out.Name)
// true gopher

Decode ​

Decode configures a custom decoder for this command. Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.

go
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 gopher

DecodeJSON ​

DecodeJSON configures JSON decoding for this command. Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.

go
type payload struct {
	Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", `{"name":"gopher"}`).
	DecodeJSON().
	Into(&out)
fmt.Println(out.Name)
// #string gopher

DecodeWith ​

DecodeWith executes the command and decodes stdout into dst.

go
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 gopher

DecodeYAML ​

DecodeYAML configures YAML decoding for this command. Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.

go
type payload struct {
	Name string `yaml:"name"`
}
var out payload
_ = execx.Command("printf", "name: gopher").
	DecodeYAML().
	Into(&out)
fmt.Println(out.Name)
// #string gopher

FromCombined ​

FromCombined decodes from combined stdout+stderr.

go
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 gopher

FromStderr ​

FromStderr decodes from stderr.

go
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 gopher

FromStdout ​

FromStdout decodes from stdout (default).

go
type payload struct {
	Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", `{"name":"gopher"}`).
	DecodeJSON().
	FromStdout().
	Into(&out)
fmt.Println(out.Name)
// #string gopher

Into ​

Into executes the command and decodes into dst.

go
type payload struct {
	Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", `{"name":"gopher"}`).
	DecodeJSON().
	Into(&out)
fmt.Println(out.Name)
// #string gopher

Trim ​

Trim trims whitespace before decoding.

go
type payload struct {
	Name string `json:"name"`
}
var out payload
_ = execx.Command("printf", "  {\"name\":\"gopher\"}  ").
	DecodeJSON().
	Trim().
	Into(&out)
fmt.Println(out.Name)
// #string gopher

Environment ​

Env ​

Env adds environment variables to the command.

go
cmd := execx.Command("go", "env", "GOOS").Env("MODE=prod")
fmt.Println(strings.Contains(strings.Join(cmd.EnvList(), ","), "MODE=prod"))
// #bool true

EnvAppend ​

EnvAppend merges variables into the inherited environment.

go
cmd := execx.Command("go", "env", "GOOS").EnvAppend(map[string]string{"A": "1"})
fmt.Println(strings.Contains(strings.Join(cmd.EnvList(), ","), "A=1"))
// #bool true

EnvInherit ​

EnvInherit restores default environment inheritance.

go
cmd := execx.Command("go", "env", "GOOS").EnvInherit()
fmt.Println(len(cmd.EnvList()) > 0)
// #bool true

EnvList ​

EnvList returns the environment list for execution.

go
cmd := execx.Command("go", "env", "GOOS").EnvOnly(map[string]string{"A": "1"})
fmt.Println(strings.Join(cmd.EnvList(), ","))
// #string A=1

EnvOnly ​

EnvOnly ignores the parent environment.

go
cmd := execx.Command("go", "env", "GOOS").EnvOnly(map[string]string{"A": "1"})
fmt.Println(strings.Join(cmd.EnvList(), ","))
// #string A=1

Errors ​

Error ​

Error returns the wrapped error message when available.

go
err := execx.ErrExec{Err: fmt.Errorf("boom")}
fmt.Println(err.Error())
// #string boom

Unwrap ​

Unwrap exposes the underlying error.

go
err := execx.ErrExec{Err: fmt.Errorf("boom")}
fmt.Println(err.Unwrap() != nil)
// #bool true

Execution ​

CombinedOutput ​

CombinedOutput executes the command and returns stdout and stderr in observed chunk order plus any execution error. Exact byte interleaving between the two operating-system streams is inherently scheduler-dependent.

go
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.
// false

OnExecCmd ​

OnExecCmd registers a callback to mutate the underlying exec.Cmd before execx attaches its stdin, output capture, and pipeline wiring. Use it for fields such as SysProcAttr; execx owns Stdin, Stdout, and Stderr during execution.

go
_, _ = execx.Command("printf", "hi").
	OnExecCmd(func(cmd *exec.Cmd) {
		cmd.SysProcAttr = &syscall.SysProcAttr{}
	}).
	Run()

Output ​

Output executes the command and returns stdout and any error.

go
out, _ := execx.Command("printf", "hello").Output()
fmt.Print(out)
// hello

OutputBytes ​

OutputBytes executes the command and returns stdout bytes and any error.

go
out, _ := execx.Command("printf", "hello").OutputBytes()
fmt.Println(string(out))
// #string hello

OutputTrimmed ​

OutputTrimmed executes the command and returns trimmed stdout and any error.

go
out, _ := execx.Command("printf", "hello\n").OutputTrimmed()
fmt.Println(out)
// #string hello

Run ​

Run executes the command and returns the result and any execution error. A non-zero exit code is represented in Result and does not itself produce an error.

go
res, _ := execx.Command("go", "env", "GOOS").Run()
fmt.Println(res.ExitCode == 0)
// #bool true

Start ​

Start executes the command asynchronously. Startup still completes synchronously, so a returned Process represents either a fully started pipeline or a completed error.

go
proc := execx.Command("go", "env", "GOOS").Start()
res, _ := proc.Wait()
fmt.Println(res.ExitCode == 0)
// #bool true

Input ​

StdinBytes ​

StdinBytes sets stdin from a copy of bytes so later caller mutation cannot change the command input.

go
out, _ := execx.Command("cat").
	StdinBytes([]byte("hi")).
	Output()
fmt.Println(out)
// #string hi

StdinFile ​

StdinFile sets stdin from a file.

go
file, _ := os.CreateTemp("", "execx-stdin")
_, _ = file.WriteString("hi")
_, _ = file.Seek(0, 0)
out, _ := execx.Command("cat").
	StdinFile(file).
	Output()
fmt.Println(out)
// #string hi

StdinReader ​

StdinReader sets stdin from an io.Reader.

go
out, _ := execx.Command("cat").
	StdinReader(strings.NewReader("hi")).
	Output()
fmt.Println(out)
// #string hi

StdinString ​

StdinString sets stdin from a string.

go
out, _ := execx.Command("cat").
	StdinString("hi").
	Output()
fmt.Println(out)
// #string hi

OS Controls ​

CreationFlags ​

CreationFlags is a no-op on non-Windows platforms; on Windows it sets process creation flags.

go
out, _ := execx.Command("printf", "ok").CreationFlags(execx.CreateNewProcessGroup).Output()
fmt.Print(out)
// ok

HideWindow ​

HideWindow is a no-op on non-Windows platforms; on Windows it hides console windows.

go
out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
fmt.Print(out)
// ok

Pdeathsig ​

Pdeathsig is a no-op on non-Linux platforms; on Linux it signals the child when the parent exits.

go
out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
fmt.Print(out)
// ok

Setpgid ​

Setpgid places the child in a new process group for group signals.

go
out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
fmt.Print(out)
// ok

Setsid ​

Setsid starts the child in a new session, detaching it from the terminal.

go
out, _ := execx.Command("printf", "ok").Setsid(true).Output()
fmt.Print(out)
// ok

Pipelining ​

Pipe ​

Pipe appends a new command to the pipeline. Pipelines run on all platforms. Configuration called on the returned Cmd applies to the new stage; configure the current stage before Pipe when it needs different environment, context, or I/O.

go
out, _ := execx.Command("printf", "go").
	Pipe("tr", "a-z", "A-Z").
	OutputTrimmed()
fmt.Println(out)
// #string GO

PipeBestEffort ​

PipeBestEffort selects the last stage as the primary result while surfacing the first execution error. Every stage is started concurrently.

go
res, _ := execx.Command("false").
	Pipe("printf", "ok").
	PipeBestEffort().
	Run()
fmt.Print(res.Stdout)
// ok

PipeStrict ​

PipeStrict selects the first stage with an execution error or non-zero exit as the primary result. Every stage is still started concurrently.

go
res, _ := execx.Command("false").
	Pipe("printf", "ok").
	PipeStrict().
	Run()
fmt.Println(res.ExitCode != 0)
// #bool true

PipelineResults ​

PipelineResults executes the command and returns per-stage results and the first execution error. Non-zero exit codes remain data in their corresponding Result.

go
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.

go
proc := execx.Command("sleep", "2").Start()
_ = proc.GracefulShutdown(os.Interrupt, 100*time.Millisecond)
res, _ := proc.Wait()
fmt.Println(res.IsSignal(os.Interrupt))
// #bool true

Interrupt ​

Interrupt sends an interrupt signal to the process.

go
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.

go
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.

go
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.

go
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.

go
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.

go
res, _ := execx.Command("go", "env", "GOOS").Run()
fmt.Println(res.IsExitCode(0))
// #bool true

IsSignal ​

IsSignal reports whether the command terminated due to a signal.

go
res, _ := execx.Command("go", "env", "GOOS").Run()
fmt.Println(res.IsSignal(os.Interrupt))
// false

OK ​

OK reports whether the command exited cleanly without errors.

go
res, _ := execx.Command("go", "env", "GOOS").Run()
fmt.Println(res.OK())
// #bool true

Shadow Print ​

ShadowOff ​

ShadowOff disables shadow printing for this command chain, preserving configuration.

go
_, _ = execx.Command("printf", "hi").ShadowPrint().ShadowOff().Run()

ShadowOn ​

ShadowOn enables shadow printing using the previously configured options.

go
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

go
_, _ = 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

go
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.

go
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 hi

WithMask ​

WithMask applies a masker to the shadow-printed command string.

go
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.

go
_, _ = 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. The final unterminated line is delivered after the process exits. Output callbacks and writers are serialized across one pipeline.

go
_, 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.
// false

OnStdout ​

OnStdout registers a line callback for stdout. The final unterminated line is delivered after the process exits. Output callbacks and writers are serialized across one pipeline.

go
_, _ = execx.Command("printf", "hi\n").
	OnStdout(func(line string) { fmt.Println(line) }).
	Run()
// hi

StderrWriter ​

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. Writer failures are returned as ErrExec after already received bytes are captured.

go
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.
// false

StdoutWriter ​

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. Writer failures are returned as ErrExec after already received bytes are captured.

go
var out strings.Builder
_, _ = execx.Command("printf", "hello").
	StdoutWriter(&out).
	Run()
fmt.Print(out.String())
// hello

WithPTY ​

WithPTY attaches stdout/stderr to a pseudo-terminal.

When enabled, stdout and stderr are merged into a single stream. OnStdout and OnStderr both receive the same lines, and Result.Stderr remains empty. Platforms without PTY support return an error when the command runs.

go
_, _ = execx.Command("printf", "hi").
	WithPTY().
	OnStdout(func(line string) { fmt.Println(line) }).
	Run()
// hi

WorkingDir ​

Dir ​

Dir sets the working directory.

go
dir := os.TempDir()
out, _ := execx.Command("pwd").
	Dir(dir).
	OutputTrimmed()
fmt.Println(out == dir)
// #bool true

Development ​

docs and examples are separate Go modules, keeping their tooling and generated programs out of the library module download.

Use make test, make test-race, make vet, and make generate. The test and vet targets cover all three modules; generation rebuilds the examples and README.

Licensed under the MIT License.