Skip to content
/ execx Public

execx is an ergonomic, fluent wrapper around Go’s os/exec package that makes running external commands simple, expressive, and safe.

License

Notifications You must be signed in to change notification settings

goforj/execx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

str logo

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

Go Reference License: MIT Go Test Go version Latest tag Tests Go Report Card

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/execx

Quick Start

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:

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:

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.

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.

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 true

Environment & 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 true

Standard input is opt-in:

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

Advanced 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 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

// 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)
    • 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.

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
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
WorkingDir Dir

Arguments

Arg

Arg appends arguments to the command.

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

Construction

Command

Command constructs a new command without executing it.

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

Context

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 true

WithDeadline

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 true

WithTimeout

WithTimeout binds the command to a timeout.

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.

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.

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's

Decoding

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 gopher

DecodeJSON

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 gopher

DecodeWith

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 gopher

DecodeYAML

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 gopher

FromCombined

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 gopher

FromStderr

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 gopher

FromStdout

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 gopher

Into

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 gopher

Trim

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 gopher

Environment

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 true

EnvAppend

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 true

EnvInherit

EnvInherit restores default environment inheritance.

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

EnvList

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=1

EnvOnly

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=1

Errors

Error

Error returns the wrapped error message when available.

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

Unwrap

Unwrap exposes the underlying error.

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

Execution

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

Output

Output executes the command and returns stdout and any error.

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

OutputBytes

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

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

OutputTrimmed

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

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

Run

Run executes the command and returns the result and any error.

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

Start

Start executes the command asynchronously.

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

Input

StdinBytes

StdinBytes sets stdin from bytes.

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

StdinFile

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 hi

StdinReader

StdinReader sets stdin from an io.Reader.

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

StdinString

StdinString sets stdin from a string.

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.

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.

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.

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.

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.

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.

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

PipeBestEffort

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)
// ok

PipeStrict

PipeStrict sets strict pipeline semantics (stop on first failure).

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

Interrupt

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 true

IsSignal

IsSignal reports whether the command terminated due to a signal.

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

OK

OK reports whether the command exited cleanly without errors.

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.

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

WithMask

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

OnStdout

OnStdout registers a line callback for stdout.

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

StderrWriter

StderrWriter sets a raw writer for stderr.

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.

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

WorkingDir

Dir

Dir sets the working directory.

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

About

execx is an ergonomic, fluent wrapper around Go’s os/exec package that makes running external commands simple, expressive, and safe.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages