From e32f8754f801ff54a9c774794393b6864ea56b39 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 14:54:42 -0600
Subject: [PATCH 01/11] docs: readme adjustments
---
README.md | 239 ++++++++++++++----------------------------------------
1 file changed, 61 insertions(+), 178 deletions(-)
diff --git a/README.md b/README.md
index 02a1b68..132f0c1 100644
--- a/README.md
+++ b/README.md
@@ -19,246 +19,129 @@
-It provides a clean, composable API for running system commands without sacrificing control, correctness, or transparency.
-No magic. No hidden behavior. Just a better way to work with processes.
+## What execx is
-## Why execx?
+execx is a small, explicit wrapper around `os/exec`. It keeps the `exec.Cmd` model but adds fluent construction and consistent result handling.
-The standard library’s `os/exec` package is powerful, but verbose and easy to misuse.
-`execx` keeps the same underlying model while making the common cases obvious and safe.
-
-**execx is for you if you want:**
-
-- Clear, chainable command construction
-- Predictable execution semantics
-- Explicit control over arguments, environment, and I/O
-- Zero shell interpolation or magic
-- A small, auditable API surface
+There is no shell interpolation. Arguments, environment, and I/O are set directly, and nothing runs until you call `Run`, `Output`, or `Start`.
## Installation
```bash
go get github.com/goforj/execx
-````
+```
## Quick Start
```go
-out, err := execx.
- Command("git", "status").
- Output()
-
+out, _ := execx.Command("echo", "hello").OutputTrimmed()
fmt.Println(out)
+// #string hello
```
-Or with structured execution:
-
-```go
-res, err := execx.Command("ls", "-la").Run()
-if err != nil {
- log.Fatal(err)
-}
-
-fmt.Println(res.Stdout)
-```
+On Windows, use `cmd /c echo hello` or `powershell -Command "echo hello"` for shell built-ins.
-## Fluent Command Construction
+## Basic usage
-Commands are built fluently and executed explicitly.
+Build a command and run it:
```go
-cmd := execx.
- Command("docker", "run").
- Arg("--rm").
- Arg("-p", "8080:80").
- Arg("nginx")
+cmd := execx.Command("echo").Arg("hello")
+res, _ := cmd.Run()
+fmt.Print(res.Stdout)
+// hello
```
-Nothing is executed until you call `Run`, `Output`, or `Start`.
-
-## Argument Handling
-
Arguments are appended deterministically and never shell-expanded.
-```go
-cmd.Arg("--env", "PROD")
-cmd.Arg(map[string]string{"--name": "api"})
-```
-
-This guarantees predictable behavior across platforms.
-
-## Execution Modes
-
-### Run
-
-Execute and return a structured result:
-
-```go
-_, _ = cmd.Run()
-```
-
-### Output Variants
-
-```go
-out, err := cmd.Output()
-out, err := cmd.OutputBytes()
-out, err := cmd.OutputTrimmed()
-out, err := cmd.CombinedOutput()
-```
-
-### Output
+## Output handling
-Return stdout directly:
+Use `Output` variants when you only need stdout:
```go
-out, err := cmd.Output()
-```
-
-### Start (async)
-
-```go
-proc := cmd.Start()
-_, _ = proc.Wait()
-proc.KillAfter(5 * time.Second)
-```
-
-## Result Object
-
-Every execution returns a `Result`:
-
-```go
-type Result struct {
- Stdout string
- Stderr string
- ExitCode int
- Err error
- Duration time.Duration
-}
+out, _ := execx.Command("echo", "hello").OutputTrimmed()
+fmt.Println(out)
+// #string hello
```
-* Non-zero exit codes do **not** imply failure
-* `Err` mirrors the returned error (spawn, context, signal)
+`Output`, `OutputBytes`, `OutputTrimmed`, and `CombinedOutput` differ only in how they return data.
## Pipelining
-Chain commands safely (pipeline execution is cross-platform; the commands you choose must exist on that OS):
+Pipelines run on all platforms; command availability is OS-specific.
```go
-out, err := execx.
- Command("ps", "aux").
- Pipe("grep", "nginx").
- Pipe("awk", "{print $2}").
- Output()
+out, _ := execx.Command("printf", "go").
+ Pipe("tr", "a-z", "A-Z").
+ OutputTrimmed()
+fmt.Println(out)
+// #string GO
```
-Pipelines are explicit and deterministic. `PipeStrict` stops at the first failing stage and returns its error. `PipeBestEffort` still runs all stages, returns the last stage output, and surfaces the first error if any stage failed.
+On Windows, use `cmd /c` or `powershell -Command` for shell built-ins.
-On Windows, use `cmd /c` or `powershell -Command` to access shell built-ins when needed.
+`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.
-```go
-cmd := execx.Command("ps", "aux").Pipe("grep", "nginx")
-cmd.PipeStrict() // default
-cmd.PipeBestEffort() // returns last stage, surfaces first error
-```
-
-## Context & Timeouts
+## Context & cancellation
```go
-ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
-
-execx.Command("sleep", "5").
- WithContext(ctx).
- Run()
-
-execx.Command("sleep", "5").
- WithTimeout(2 * time.Second).
- Run()
-
-execx.Command("sleep", "5").
- WithDeadline(time.Now().Add(2 * time.Second)).
- Run()
+res, _ := execx.Command("go", "env", "GOOS").WithContext(ctx).Run()
+fmt.Println(res.ExitCode == 0)
+// #bool true
```
-## Environment Control
+## Environment & I/O control
-```go
-cmd.Env("DEBUG=true")
-cmd.Env(map[string]string{"MODE": "prod"})
-cmd.EnvOnly(map[string]string{"MODE": "prod"})
-cmd.EnvInherit()
-cmd.EnvAppend(map[string]string{"DEBUG": "1"})
-```
-
-## Streaming Output
+Environment is explicit and deterministic:
```go
-cmd.
- OnStdout(func(line string) {
- fmt.Println("OUT:", line)
- }).
- OnStderr(func(line string) {
- fmt.Println("ERR:", line)
- }).
- Run()
+cmd := execx.Command("echo", "hello").Env("MODE=prod")
+fmt.Println(strings.Contains(strings.Join(cmd.EnvList(), ","), "MODE=prod"))
+// #bool true
```
-## Raw Writers
+Standard input is opt-in:
```go
-cmd.StdoutWriter(os.Stdout)
-cmd.StderrWriter(os.Stderr)
+out, _ := execx.Command("cat").
+ StdinString("hi").
+ OutputTrimmed()
+fmt.Println(out)
+// #string hi
```
-## Exit Handling
-
-```go
-if res.IsExitCode(1) {
- log.Println("Command failed")
-}
-```
+## Advanced features
-## Debugging Helpers
+For process control, use `Start` with the `Process` helpers:
```go
-cmd.Args()
-cmd.EnvList()
-cmd.ShellEscaped()
+proc := execx.Command("go", "env", "GOOS").Start()
+res, _ := proc.Wait()
+fmt.Println(res.ExitCode == 0)
+// #bool true
```
-## Design Principles
+Signals, timeouts, and OS controls are documented in the API section below.
-* **Explicit over implicit**
-* **No hidden behavior**
-* **No shell magic**
-* **Composable over clever**
-* **Predictable over flexible**
+## Non-goals and design principles
-`execx` is intentionally boring — in the best possible way.
+Design principles:
-## Non-Goals
+* 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
-## Testing & Reliability
-
-* 100% public API coverage
-* Deterministic behavior
-* No global state
-* Safe for concurrent read-only use; mutation during execution is undefined
-
-## Runnable examples
-
-Every function has a corresponding runnable example under [`./examples`](./examples).
-
-These examples are **generated directly from the documentation blocks** of each function, ensuring the docs and code never drift. These are the same examples you see here in the README and GoDoc.
-
-An automated test executes **every example** to verify it builds and runs successfully.
-
-This guarantees all examples are valid, up-to-date, and remain functional as the API evolves.
+All public APIs are covered by runnable examples under `./examples`, and the test suite executes them to keep docs and behavior in sync.
@@ -604,7 +487,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
### Pdeathsig
-Pdeathsig sets a parent-death signal on Linux.
+Pdeathsig is a no-op on non-Linux Unix platforms.
_Example: pdeathsig_
@@ -850,7 +733,7 @@ fmt.Println(err == nil)
// flag provided but not defined: -badflag
// usage: go env [-json] [-changed] [-u] [-w] [var ...]
// Run 'go help env' for details.
-// true
+// false
```
### OnStdout
From 03392ab4d89e30cc91ea9564d1b0e90998c364b6 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 15:14:24 -0600
Subject: [PATCH 02/11] docs: more
---
README.md | 30 +++++++++++++++++++++++++++---
examples/creationflags/main.go | 2 +-
examples/hidewindow/main.go | 2 +-
examples/onstderr/main.go | 2 +-
examples/onstdout/main.go | 4 ++--
execx.go | 6 +++---
6 files changed, 35 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 132f0c1..0fb0b96 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,30 @@ fmt.Println(res.ExitCode == 0)
Signals, timeouts, and OS controls are documented in the API section below.
+## Kitchen Sink Chaining Example
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+defer cancel()
+
+res := 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", res.Err)
+}
+```
+
## Non-goals and design principles
Design principles:
@@ -487,7 +511,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
### Pdeathsig
-Pdeathsig is a no-op on non-Linux Unix platforms.
+Pdeathsig sets a parent-death signal on Linux.
_Example: pdeathsig_
@@ -741,10 +765,10 @@ fmt.Println(err == nil)
OnStdout registers a line callback for stdout.
```go
-_, _ = execx.Command("go", "env", "GOOS").
+_, _ = execx.Command("printf", "hi\n").
OnStdout(func(line string) { fmt.Println(line) }).
Run()
-// darwin
+// hi
```
### StderrWriter
diff --git a/examples/creationflags/main.go b/examples/creationflags/main.go
index fbcee1e..00d6b15 100644
--- a/examples/creationflags/main.go
+++ b/examples/creationflags/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // CreationFlags sets Windows creation flags.
+ // CreationFlags is a no-op on non-Windows platforms.
// Example: creation flags
fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
diff --git a/examples/hidewindow/main.go b/examples/hidewindow/main.go
index 7de6212..9bd0b7c 100644
--- a/examples/hidewindow/main.go
+++ b/examples/hidewindow/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // HideWindow controls window visibility and sets CREATE_NO_WINDOW for console apps.
+ // HideWindow is a no-op on non-Windows platforms.
// Example: hide window
fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
diff --git a/examples/onstderr/main.go b/examples/onstderr/main.go
index 6239f9e..db28ccf 100644
--- a/examples/onstderr/main.go
+++ b/examples/onstderr/main.go
@@ -21,5 +21,5 @@ func main() {
// flag provided but not defined: -badflag
// usage: go env [-json] [-changed] [-u] [-w] [var ...]
// Run 'go help env' for details.
- // true
+ // false
}
diff --git a/examples/onstdout/main.go b/examples/onstdout/main.go
index 93c92c7..46de6f4 100644
--- a/examples/onstdout/main.go
+++ b/examples/onstdout/main.go
@@ -12,8 +12,8 @@ func main() {
// OnStdout registers a line callback for stdout.
// Example: stdout lines
- _, _ = execx.Command("go", "env", "GOOS").
+ _, _ = execx.Command("printf", "hi\n").
OnStdout(func(line string) { fmt.Println(line) }).
Run()
- // darwin
+ // hi
}
diff --git a/execx.go b/execx.go
index 20e5520..3fc0689 100644
--- a/execx.go
+++ b/execx.go
@@ -338,10 +338,10 @@ func (c *Cmd) StdinFile(file *os.File) *Cmd {
//
// Example: stdout lines
//
-// _, _ = execx.Command("go", "env", "GOOS").
+// _, _ = execx.Command("printf", "hi\n").
// OnStdout(func(line string) { fmt.Println(line) }).
// Run()
-// // darwin
+// // hi
func (c *Cmd) OnStdout(fn func(string)) *Cmd {
c.onStdout = fn
return c
@@ -361,7 +361,7 @@ func (c *Cmd) OnStdout(fn func(string)) *Cmd {
// // flag provided but not defined: -badflag
// // usage: go env [-json] [-changed] [-u] [-w] [var ...]
// // Run 'go help env' for details.
-// // true
+// // false
func (c *Cmd) OnStderr(fn func(string)) *Cmd {
c.onStderr = fn
return c
From 8b747f09d1617aa291643e4c7e03e3e4ea897b5a Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 15:19:30 -0600
Subject: [PATCH 03/11] docs: kitchen sink example
---
README.md | 23 ++++++++++++++---
examples/kitchensink/main.go | 50 ++++++++++++++++++++++++++++++++++++
2 files changed, 69 insertions(+), 4 deletions(-)
create mode 100644 examples/kitchensink/main.go
diff --git a/README.md b/README.md
index 0fb0b96..db606a6 100644
--- a/README.md
+++ b/README.md
@@ -129,10 +129,12 @@ Signals, timeouts, and OS controls are documented in the API section below.
## 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 := execx.
+res, err := execx.
Command("printf", "hello\nworld\n").
Pipe("tr", "a-z", "A-Z").
Env("MODE=demo").
@@ -145,9 +147,22 @@ res := execx.
}).
Run()
-if !res.OK() {
- log.Fatalf("command failed: %v", res.Err)
-}
+ 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:
+ // Duration: 10.123456ms
```
## Non-goals and design principles
diff --git a/examples/kitchensink/main.go b/examples/kitchensink/main.go
new file mode 100644
index 0000000..7f4e4f5
--- /dev/null
+++ b/examples/kitchensink/main.go
@@ -0,0 +1,50 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "github.com/goforj/execx"
+ "log"
+ "time"
+)
+
+func main() {
+ // 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:
+ // Duration: 10.123456ms
+
+}
From 6e586af8a7a0d393071613aa02362dfe0e9c6678 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 15:36:02 -0600
Subject: [PATCH 04/11] docs: examples
---
README.md | 50 ++++++++++++++++++---------------
examples/arg/main.go | 6 ++--
examples/combinedoutput/main.go | 10 +++++--
examples/command/main.go | 6 ++--
examples/creationflags/main.go | 2 +-
examples/hidewindow/main.go | 2 +-
examples/output/main.go | 6 ++--
examples/outputbytes/main.go | 6 ++--
examples/outputtrimmed/main.go | 6 ++--
examples/pdeathsig/main.go | 2 +-
examples/stderrwriter/main.go | 2 +-
examples/stdoutwriter/main.go | 6 ++--
execx.go | 48 ++++++++++++++++---------------
13 files changed, 82 insertions(+), 70 deletions(-)
diff --git a/README.md b/README.md
index db606a6..5e5aab9 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -211,10 +211,10 @@ All public APIs are covered by runnable examples under `./examples`, and the tes
Arg appends arguments to the command.
```go
-cmd := execx.Command("go", "env").Arg("GOOS")
+cmd := execx.Command("printf").Arg("hello")
out, _ := cmd.Output()
-fmt.Println(out != "")
-// #bool true
+fmt.Print(out)
+// hello
```
## Construction
@@ -224,10 +224,10 @@ fmt.Println(out != "")
Command constructs a new command without executing it.
```go
-cmd := execx.Command("go", "env", "GOOS")
+cmd := execx.Command("printf", "hello")
out, _ := cmd.Output()
-fmt.Println(out != "")
-// #bool true
+fmt.Print(out)
+// hello
```
## Context
@@ -377,9 +377,13 @@ fmt.Println(err.Unwrap() != nil)
CombinedOutput executes the command and returns stdout+stderr and any error.
```go
-out, _ := execx.Command("go", "env", "GOOS").CombinedOutput()
-fmt.Println(out != "")
-// #bool true
+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
@@ -387,9 +391,9 @@ fmt.Println(out != "")
Output executes the command and returns stdout and any error.
```go
-out, _ := execx.Command("go", "env", "GOOS").Output()
-fmt.Println(out != "")
-// #bool true
+out, _ := execx.Command("printf", "hello").Output()
+fmt.Print(out)
+// hello
```
### OutputBytes
@@ -397,9 +401,9 @@ fmt.Println(out != "")
OutputBytes executes the command and returns stdout bytes and any error.
```go
-out, _ := execx.Command("go", "env", "GOOS").OutputBytes()
-fmt.Println(len(out) > 0)
-// #bool true
+out, _ := execx.Command("printf", "hello").OutputBytes()
+fmt.Println(string(out))
+// #string hello
```
### OutputTrimmed
@@ -407,9 +411,9 @@ fmt.Println(len(out) > 0)
OutputTrimmed executes the command and returns trimmed stdout and any error.
```go
-out, _ := execx.Command("go", "env", "GOOS").OutputTrimmed()
-fmt.Println(out != "")
-// #bool true
+out, _ := execx.Command("printf", "hello\n").OutputTrimmed()
+fmt.Println(out)
+// #string hello
```
### Run
@@ -800,7 +804,7 @@ fmt.Println(err == nil)
// flag provided but not defined: -badflag
// usage: go env [-json] [-changed] [-u] [-w] [var ...]
// Run 'go help env' for details.
-// true
+// false
```
### StdoutWriter
@@ -809,11 +813,11 @@ StdoutWriter sets a raw writer for stdout.
```go
var out strings.Builder
-_, err := execx.Command("go", "env", "GOOS").
+_, _ = execx.Command("printf", "hello").
StdoutWriter(&out).
Run()
-fmt.Println(err == nil && out.Len() > 0)
-// #bool true
+fmt.Print(out.String())
+// hello
```
## WorkingDir
diff --git a/examples/arg/main.go b/examples/arg/main.go
index c40898b..89a1f8b 100644
--- a/examples/arg/main.go
+++ b/examples/arg/main.go
@@ -12,8 +12,8 @@ func main() {
// Arg appends arguments to the command.
// Example: add args
- cmd := execx.Command("go", "env").Arg("GOOS")
+ cmd := execx.Command("printf").Arg("hello")
out, _ := cmd.Output()
- fmt.Println(out != "")
- // #bool true
+ fmt.Print(out)
+ // hello
}
diff --git a/examples/combinedoutput/main.go b/examples/combinedoutput/main.go
index d941379..4eb4a24 100644
--- a/examples/combinedoutput/main.go
+++ b/examples/combinedoutput/main.go
@@ -12,7 +12,11 @@ func main() {
// CombinedOutput executes the command and returns stdout+stderr and any error.
// Example: combined output
- out, _ := execx.Command("go", "env", "GOOS").CombinedOutput()
- fmt.Println(out != "")
- // #bool true
+ 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
}
diff --git a/examples/command/main.go b/examples/command/main.go
index d27a541..4c0f907 100644
--- a/examples/command/main.go
+++ b/examples/command/main.go
@@ -12,8 +12,8 @@ func main() {
// Command constructs a new command without executing it.
// Example: command
- cmd := execx.Command("go", "env", "GOOS")
+ cmd := execx.Command("printf", "hello")
out, _ := cmd.Output()
- fmt.Println(out != "")
- // #bool true
+ fmt.Print(out)
+ // hello
}
diff --git a/examples/creationflags/main.go b/examples/creationflags/main.go
index 00d6b15..fbcee1e 100644
--- a/examples/creationflags/main.go
+++ b/examples/creationflags/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // CreationFlags is a no-op on non-Windows platforms.
+ // CreationFlags sets Windows creation flags.
// Example: creation flags
fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
diff --git a/examples/hidewindow/main.go b/examples/hidewindow/main.go
index 9bd0b7c..7de6212 100644
--- a/examples/hidewindow/main.go
+++ b/examples/hidewindow/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // HideWindow is a no-op on non-Windows platforms.
+ // HideWindow controls window visibility and sets CREATE_NO_WINDOW for console apps.
// Example: hide window
fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
diff --git a/examples/output/main.go b/examples/output/main.go
index b593173..096607a 100644
--- a/examples/output/main.go
+++ b/examples/output/main.go
@@ -12,7 +12,7 @@ func main() {
// Output executes the command and returns stdout and any error.
// Example: output
- out, _ := execx.Command("go", "env", "GOOS").Output()
- fmt.Println(out != "")
- // #bool true
+ out, _ := execx.Command("printf", "hello").Output()
+ fmt.Print(out)
+ // hello
}
diff --git a/examples/outputbytes/main.go b/examples/outputbytes/main.go
index a79b3c6..ad7f167 100644
--- a/examples/outputbytes/main.go
+++ b/examples/outputbytes/main.go
@@ -12,7 +12,7 @@ func main() {
// OutputBytes executes the command and returns stdout bytes and any error.
// Example: output bytes
- out, _ := execx.Command("go", "env", "GOOS").OutputBytes()
- fmt.Println(len(out) > 0)
- // #bool true
+ out, _ := execx.Command("printf", "hello").OutputBytes()
+ fmt.Println(string(out))
+ // #string hello
}
diff --git a/examples/outputtrimmed/main.go b/examples/outputtrimmed/main.go
index 4042a5f..a4c47c4 100644
--- a/examples/outputtrimmed/main.go
+++ b/examples/outputtrimmed/main.go
@@ -12,7 +12,7 @@ func main() {
// OutputTrimmed executes the command and returns trimmed stdout and any error.
// Example: output trimmed
- out, _ := execx.Command("go", "env", "GOOS").OutputTrimmed()
- fmt.Println(out != "")
- // #bool true
+ out, _ := execx.Command("printf", "hello\n").OutputTrimmed()
+ fmt.Println(out)
+ // #string hello
}
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 7fa2301..67c3701 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Pdeathsig sets a parent-death signal on Linux.
+ // Pdeathsig is a no-op on non-Linux Unix platforms.
// Example: pdeathsig
fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
diff --git a/examples/stderrwriter/main.go b/examples/stderrwriter/main.go
index 5bfd581..d33cca6 100644
--- a/examples/stderrwriter/main.go
+++ b/examples/stderrwriter/main.go
@@ -22,5 +22,5 @@ func main() {
// flag provided but not defined: -badflag
// usage: go env [-json] [-changed] [-u] [-w] [var ...]
// Run 'go help env' for details.
- // true
+ // false
}
diff --git a/examples/stdoutwriter/main.go b/examples/stdoutwriter/main.go
index 1efa718..877d34c 100644
--- a/examples/stdoutwriter/main.go
+++ b/examples/stdoutwriter/main.go
@@ -14,9 +14,9 @@ func main() {
// Example: stdout writer
var out strings.Builder
- _, err := execx.Command("go", "env", "GOOS").
+ _, _ = execx.Command("printf", "hello").
StdoutWriter(&out).
Run()
- fmt.Println(err == nil && out.Len() > 0)
- // #bool true
+ fmt.Print(out.String())
+ // hello
}
diff --git a/execx.go b/execx.go
index 3fc0689..2722982 100644
--- a/execx.go
+++ b/execx.go
@@ -36,10 +36,10 @@ const (
//
// Example: command
//
-// cmd := execx.Command("go", "env", "GOOS")
+// cmd := execx.Command("printf", "hello")
// out, _ := cmd.Output()
-// fmt.Println(out != "")
-// // #bool true
+// fmt.Print(out)
+// // hello
func Command(name string, args ...string) *Cmd {
cmd := &Cmd{
name: name,
@@ -81,10 +81,10 @@ type Cmd struct {
//
// Example: add args
//
-// cmd := execx.Command("go", "env").Arg("GOOS")
+// cmd := execx.Command("printf").Arg("hello")
// out, _ := cmd.Output()
-// fmt.Println(out != "")
-// // #bool true
+// fmt.Print(out)
+// // hello
func (c *Cmd) Arg(values ...any) *Cmd {
for _, value := range values {
switch v := value.(type) {
@@ -373,11 +373,11 @@ func (c *Cmd) OnStderr(fn func(string)) *Cmd {
// Example: stdout writer
//
// var out strings.Builder
-// _, err := execx.Command("go", "env", "GOOS").
+// _, _ = execx.Command("printf", "hello").
// StdoutWriter(&out).
// Run()
-// fmt.Println(err == nil && out.Len() > 0)
-// // #bool true
+// fmt.Print(out.String())
+// // hello
func (c *Cmd) StdoutWriter(w io.Writer) *Cmd {
c.stdoutW = w
return c
@@ -397,7 +397,7 @@ func (c *Cmd) StdoutWriter(w io.Writer) *Cmd {
// // flag provided but not defined: -badflag
// // usage: go env [-json] [-changed] [-u] [-w] [var ...]
// // Run 'go help env' for details.
-// // true
+// // false
func (c *Cmd) StderrWriter(w io.Writer) *Cmd {
c.stderrW = w
return c
@@ -548,9 +548,9 @@ func (c *Cmd) Run() (Result, error) {
//
// Example: output
//
-// out, _ := execx.Command("go", "env", "GOOS").Output()
-// fmt.Println(out != "")
-// // #bool true
+// out, _ := execx.Command("printf", "hello").Output()
+// fmt.Print(out)
+// // hello
func (c *Cmd) Output() (string, error) {
result, err := c.Run()
return result.Stdout, err
@@ -561,9 +561,9 @@ func (c *Cmd) Output() (string, error) {
//
// Example: output bytes
//
-// out, _ := execx.Command("go", "env", "GOOS").OutputBytes()
-// fmt.Println(len(out) > 0)
-// // #bool true
+// out, _ := execx.Command("printf", "hello").OutputBytes()
+// fmt.Println(string(out))
+// // #string hello
func (c *Cmd) OutputBytes() ([]byte, error) {
result, err := c.Run()
return []byte(result.Stdout), err
@@ -574,9 +574,9 @@ func (c *Cmd) OutputBytes() ([]byte, error) {
//
// Example: output trimmed
//
-// out, _ := execx.Command("go", "env", "GOOS").OutputTrimmed()
-// fmt.Println(out != "")
-// // #bool true
+// out, _ := execx.Command("printf", "hello\n").OutputTrimmed()
+// fmt.Println(out)
+// // #string hello
func (c *Cmd) OutputTrimmed() (string, error) {
result, err := c.Run()
return strings.TrimSpace(result.Stdout), err
@@ -587,9 +587,13 @@ func (c *Cmd) OutputTrimmed() (string, error) {
//
// Example: combined output
//
-// out, _ := execx.Command("go", "env", "GOOS").CombinedOutput()
-// fmt.Println(out != "")
-// // #bool true
+// 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
func (c *Cmd) CombinedOutput() (string, error) {
pipe := c.newPipeline(true)
pipe.start()
From 8b1527b5c805e2b35df5e2079cf3778ffc1bf454 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 15:40:50 -0600
Subject: [PATCH 05/11] docs: tweaks
---
README.md | 26 +++++++++++++-------------
examples/creationflags/main.go | 2 +-
examples/gracefulshutdown/main.go | 4 ++--
examples/hidewindow/main.go | 2 +-
examples/interrupt/main.go | 5 +++--
examples/killafter/main.go | 4 ++--
examples/pdeathsig/main.go | 2 +-
examples/send/main.go | 4 ++--
examples/terminate/main.go | 4 ++--
execx.go | 20 ++++++++++----------
10 files changed, 37 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index 5e5aab9..a12b7e2 100644
--- a/README.md
+++ b/README.md
@@ -530,7 +530,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
### Pdeathsig
-Pdeathsig sets a parent-death signal on Linux.
+Pdeathsig is a no-op on Windows.
_Example: pdeathsig_
@@ -555,7 +555,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
### Setpgid
-Setpgid sets the process group ID behavior.
+Setpgid is a no-op on Windows.
_Example: setpgid_
@@ -580,7 +580,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
### Setsid
-Setsid sets the session ID behavior.
+Setsid is a no-op on Windows.
_Example: setsid_
@@ -664,8 +664,8 @@ 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, err := proc.Wait()
-fmt.Println(err != nil || res.ExitCode != 0)
+res, _ := proc.Wait()
+fmt.Println(res.IsSignal(os.Interrupt))
// #bool true
```
@@ -676,8 +676,8 @@ Interrupt sends an interrupt signal to the process.
```go
proc := execx.Command("sleep", "2").Start()
_ = proc.Interrupt()
-res, err := proc.Wait()
-fmt.Println(err != nil || res.ExitCode != 0)
+res, _ := proc.Wait()
+fmt.Println(res.IsSignal(os.Interrupt))
// #bool true
```
@@ -688,8 +688,8 @@ KillAfter terminates the process after the given duration.
```go
proc := execx.Command("sleep", "2").Start()
proc.KillAfter(100 * time.Millisecond)
-res, err := proc.Wait()
-fmt.Println(err != nil || res.ExitCode != 0)
+res, _ := proc.Wait()
+fmt.Println(res.ExitCode != 0)
// #bool true
```
@@ -700,8 +700,8 @@ Send sends a signal to the process.
```go
proc := execx.Command("sleep", "2").Start()
_ = proc.Send(os.Interrupt)
-res, err := proc.Wait()
-fmt.Println(err != nil || res.ExitCode != 0)
+res, _ := proc.Wait()
+fmt.Println(res.IsSignal(os.Interrupt))
// #bool true
```
@@ -712,8 +712,8 @@ Terminate kills the process immediately.
```go
proc := execx.Command("sleep", "2").Start()
_ = proc.Terminate()
-res, err := proc.Wait()
-fmt.Println(err != nil || res.ExitCode != 0)
+res, _ := proc.Wait()
+fmt.Println(res.ExitCode != 0)
// #bool true
```
diff --git a/examples/creationflags/main.go b/examples/creationflags/main.go
index fbcee1e..00d6b15 100644
--- a/examples/creationflags/main.go
+++ b/examples/creationflags/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // CreationFlags sets Windows creation flags.
+ // CreationFlags is a no-op on non-Windows platforms.
// Example: creation flags
fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
diff --git a/examples/gracefulshutdown/main.go b/examples/gracefulshutdown/main.go
index 9a7ebcb..e2278c1 100644
--- a/examples/gracefulshutdown/main.go
+++ b/examples/gracefulshutdown/main.go
@@ -16,7 +16,7 @@ func main() {
// Example: graceful shutdown
proc := execx.Command("sleep", "2").Start()
_ = proc.GracefulShutdown(os.Interrupt, 100*time.Millisecond)
- res, err := proc.Wait()
- fmt.Println(err != nil || res.ExitCode != 0)
+ res, _ := proc.Wait()
+ fmt.Println(res.IsSignal(os.Interrupt))
// #bool true
}
diff --git a/examples/hidewindow/main.go b/examples/hidewindow/main.go
index 7de6212..9bd0b7c 100644
--- a/examples/hidewindow/main.go
+++ b/examples/hidewindow/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // HideWindow controls window visibility and sets CREATE_NO_WINDOW for console apps.
+ // HideWindow is a no-op on non-Windows platforms.
// Example: hide window
fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
diff --git a/examples/interrupt/main.go b/examples/interrupt/main.go
index e30f8c7..81a4368 100644
--- a/examples/interrupt/main.go
+++ b/examples/interrupt/main.go
@@ -6,6 +6,7 @@ package main
import (
"fmt"
"github.com/goforj/execx"
+ "os"
)
func main() {
@@ -14,7 +15,7 @@ func main() {
// Example: interrupt
proc := execx.Command("sleep", "2").Start()
_ = proc.Interrupt()
- res, err := proc.Wait()
- fmt.Println(err != nil || res.ExitCode != 0)
+ res, _ := proc.Wait()
+ fmt.Println(res.IsSignal(os.Interrupt))
// #bool true
}
diff --git a/examples/killafter/main.go b/examples/killafter/main.go
index 3d2471b..ceb0723 100644
--- a/examples/killafter/main.go
+++ b/examples/killafter/main.go
@@ -15,7 +15,7 @@ func main() {
// Example: kill after
proc := execx.Command("sleep", "2").Start()
proc.KillAfter(100 * time.Millisecond)
- res, err := proc.Wait()
- fmt.Println(err != nil || res.ExitCode != 0)
+ res, _ := proc.Wait()
+ fmt.Println(res.ExitCode != 0)
// #bool true
}
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 67c3701..7fa2301 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Pdeathsig is a no-op on non-Linux Unix platforms.
+ // Pdeathsig sets a parent-death signal on Linux.
// Example: pdeathsig
fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
diff --git a/examples/send/main.go b/examples/send/main.go
index 1ffbffa..de7e060 100644
--- a/examples/send/main.go
+++ b/examples/send/main.go
@@ -15,7 +15,7 @@ func main() {
// Example: send signal
proc := execx.Command("sleep", "2").Start()
_ = proc.Send(os.Interrupt)
- res, err := proc.Wait()
- fmt.Println(err != nil || res.ExitCode != 0)
+ res, _ := proc.Wait()
+ fmt.Println(res.IsSignal(os.Interrupt))
// #bool true
}
diff --git a/examples/terminate/main.go b/examples/terminate/main.go
index eb0603e..2efba30 100644
--- a/examples/terminate/main.go
+++ b/examples/terminate/main.go
@@ -14,7 +14,7 @@ func main() {
// Example: terminate
proc := execx.Command("sleep", "2").Start()
_ = proc.Terminate()
- res, err := proc.Wait()
- fmt.Println(err != nil || res.ExitCode != 0)
+ res, _ := proc.Wait()
+ fmt.Println(res.ExitCode != 0)
// #bool true
}
diff --git a/execx.go b/execx.go
index 2722982..aded774 100644
--- a/execx.go
+++ b/execx.go
@@ -806,8 +806,8 @@ func (p *Process) Wait() (Result, error) {
//
// proc := execx.Command("sleep", "2").Start()
// proc.KillAfter(100 * time.Millisecond)
-// res, err := proc.Wait()
-// fmt.Println(err != nil || res.ExitCode != 0)
+// res, _ := proc.Wait()
+// fmt.Println(res.ExitCode != 0)
// // #bool true
func (p *Process) KillAfter(d time.Duration) {
p.mu.Lock()
@@ -827,8 +827,8 @@ func (p *Process) KillAfter(d time.Duration) {
//
// proc := execx.Command("sleep", "2").Start()
// _ = proc.Send(os.Interrupt)
-// res, err := proc.Wait()
-// fmt.Println(err != nil || res.ExitCode != 0)
+// res, _ := proc.Wait()
+// fmt.Println(res.IsSignal(os.Interrupt))
// // #bool true
func (p *Process) Send(sig os.Signal) error {
return p.signalAll(func(proc *os.Process) error {
@@ -843,8 +843,8 @@ func (p *Process) Send(sig os.Signal) error {
//
// proc := execx.Command("sleep", "2").Start()
// _ = proc.Interrupt()
-// res, err := proc.Wait()
-// fmt.Println(err != nil || res.ExitCode != 0)
+// res, _ := proc.Wait()
+// fmt.Println(res.IsSignal(os.Interrupt))
// // #bool true
func (p *Process) Interrupt() error {
return p.Send(os.Interrupt)
@@ -857,8 +857,8 @@ func (p *Process) Interrupt() error {
//
// proc := execx.Command("sleep", "2").Start()
// _ = proc.Terminate()
-// res, err := proc.Wait()
-// fmt.Println(err != nil || res.ExitCode != 0)
+// res, _ := proc.Wait()
+// fmt.Println(res.ExitCode != 0)
// // #bool true
func (p *Process) Terminate() error {
return p.signalAll(func(proc *os.Process) error {
@@ -873,8 +873,8 @@ func (p *Process) Terminate() error {
//
// proc := execx.Command("sleep", "2").Start()
// _ = proc.GracefulShutdown(os.Interrupt, 100*time.Millisecond)
-// res, err := proc.Wait()
-// fmt.Println(err != nil || res.ExitCode != 0)
+// res, _ := proc.Wait()
+// fmt.Println(res.IsSignal(os.Interrupt))
// // #bool true
func (p *Process) GracefulShutdown(sig os.Signal, timeout time.Duration) error {
if timeout <= 0 {
From aed9a50afcd46ba78cba9d25f18aedf8cdae6488 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 15:49:57 -0600
Subject: [PATCH 06/11] docs: more changes
---
README.md | 21 ++++++++++++---------
examples/creationflags/main.go | 2 +-
examples/hidewindow/main.go | 2 +-
examples/pdeathsig/main.go | 2 +-
examples/pipebesteffort/main.go | 6 +++---
examples/pipelineresults/main.go | 9 ++++++---
examples/setpgid/main.go | 2 +-
examples/setsid/main.go | 2 +-
execx.go | 15 +++++++++------
9 files changed, 35 insertions(+), 26 deletions(-)
diff --git a/README.md b/README.md
index a12b7e2..3e745f7 100644
--- a/README.md
+++ b/README.md
@@ -530,7 +530,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
### Pdeathsig
-Pdeathsig is a no-op on Windows.
+Pdeathsig sets a parent-death signal on Linux.
_Example: pdeathsig_
@@ -555,7 +555,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
### Setpgid
-Setpgid is a no-op on Windows.
+Setpgid sets the process group ID behavior.
_Example: setpgid_
@@ -580,7 +580,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
### Setsid
-Setsid is a no-op on Windows.
+Setsid sets the session ID behavior.
_Example: setsid_
@@ -622,12 +622,12 @@ fmt.Println(out)
PipeBestEffort sets best-effort pipeline semantics (run all stages, surface the first error).
```go
-res, err := execx.Command("false").
+res, _ := execx.Command("false").
Pipe("printf", "ok").
PipeBestEffort().
Run()
-fmt.Println(err == nil && res.Stdout == "ok")
-// #bool true
+fmt.Print(res.Stdout)
+// ok
```
### PipeStrict
@@ -648,11 +648,14 @@ fmt.Println(res.ExitCode != 0)
PipelineResults executes the command and returns per-stage results and any error.
```go
-results, err := execx.Command("printf", "go").
+results, _ := execx.Command("printf", "go").
Pipe("tr", "a-z", "A-Z").
PipelineResults()
-fmt.Println(err == nil && len(results) == 2)
-// #bool true
+fmt.Printf("%+v", results)
+// [
+// {Stdout:go Stderr: ExitCode:0 Err: Duration:6.367208ms signal:}
+// {Stdout:GO Stderr: ExitCode:0 Err: Duration:4.976291ms signal:}
+// ]
```
## Process
diff --git a/examples/creationflags/main.go b/examples/creationflags/main.go
index 00d6b15..fbcee1e 100644
--- a/examples/creationflags/main.go
+++ b/examples/creationflags/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // CreationFlags is a no-op on non-Windows platforms.
+ // CreationFlags sets Windows creation flags.
// Example: creation flags
fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
diff --git a/examples/hidewindow/main.go b/examples/hidewindow/main.go
index 9bd0b7c..7de6212 100644
--- a/examples/hidewindow/main.go
+++ b/examples/hidewindow/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // HideWindow is a no-op on non-Windows platforms.
+ // HideWindow controls window visibility and sets CREATE_NO_WINDOW for console apps.
// Example: hide window
fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 7fa2301..5e652c2 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Pdeathsig sets a parent-death signal on Linux.
+ // Pdeathsig is a no-op on Windows.
// Example: pdeathsig
fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
diff --git a/examples/pipebesteffort/main.go b/examples/pipebesteffort/main.go
index 39c477a..f518bdf 100644
--- a/examples/pipebesteffort/main.go
+++ b/examples/pipebesteffort/main.go
@@ -12,10 +12,10 @@ func main() {
// PipeBestEffort sets best-effort pipeline semantics (run all stages, surface the first error).
// Example: best effort
- res, err := execx.Command("false").
+ res, _ := execx.Command("false").
Pipe("printf", "ok").
PipeBestEffort().
Run()
- fmt.Println(err == nil && res.Stdout == "ok")
- // #bool true
+ fmt.Print(res.Stdout)
+ // ok
}
diff --git a/examples/pipelineresults/main.go b/examples/pipelineresults/main.go
index b3d7c07..609cd56 100644
--- a/examples/pipelineresults/main.go
+++ b/examples/pipelineresults/main.go
@@ -12,9 +12,12 @@ func main() {
// PipelineResults executes the command and returns per-stage results and any error.
// Example: pipeline results
- results, err := execx.Command("printf", "go").
+ results, _ := execx.Command("printf", "go").
Pipe("tr", "a-z", "A-Z").
PipelineResults()
- fmt.Println(err == nil && len(results) == 2)
- // #bool true
+ fmt.Printf("%+v", results)
+ // [
+ // {Stdout:go Stderr: ExitCode:0 Err: Duration:6.367208ms signal:}
+ // {Stdout:GO Stderr: ExitCode:0 Err: Duration:4.976291ms signal:}
+ // ]
}
diff --git a/examples/setpgid/main.go b/examples/setpgid/main.go
index fac2709..7b3c63c 100644
--- a/examples/setpgid/main.go
+++ b/examples/setpgid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setpgid sets the process group ID behavior.
+ // Setpgid is a no-op on Windows.
// Example: setpgid
fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
diff --git a/examples/setsid/main.go b/examples/setsid/main.go
index 4ee527b..3f1451a 100644
--- a/examples/setsid/main.go
+++ b/examples/setsid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setsid sets the session ID behavior.
+ // Setsid is a no-op on Windows.
// Example: setsid
fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
diff --git a/execx.go b/execx.go
index aded774..13e8c89 100644
--- a/execx.go
+++ b/execx.go
@@ -451,12 +451,12 @@ func (c *Cmd) PipeStrict() *Cmd {
//
// Example: best effort
//
-// res, err := execx.Command("false").
+// res, _ := execx.Command("false").
// Pipe("printf", "ok").
// PipeBestEffort().
// Run()
-// fmt.Println(err == nil && res.Stdout == "ok")
-// // #bool true
+// fmt.Print(res.Stdout)
+// // ok
func (c *Cmd) PipeBestEffort() *Cmd {
c.rootCmd().pipeMode = pipeBestEffort
return c
@@ -607,11 +607,14 @@ func (c *Cmd) CombinedOutput() (string, error) {
//
// Example: pipeline results
//
-// results, err := execx.Command("printf", "go").
+// results, _ := execx.Command("printf", "go").
// Pipe("tr", "a-z", "A-Z").
// PipelineResults()
-// fmt.Println(err == nil && len(results) == 2)
-// // #bool true
+// fmt.Printf("%+v", results)
+// // [
+// // {Stdout:go Stderr: ExitCode:0 Err: Duration:6.367208ms signal:}
+// // {Stdout:GO Stderr: ExitCode:0 Err: Duration:4.976291ms signal:}
+// // ]
func (c *Cmd) PipelineResults() ([]Result, error) {
pipe := c.newPipeline(false)
pipe.start()
From 729b2161d403cbb2dbb09554f5de0add66edfb01 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 15:53:08 -0600
Subject: [PATCH 07/11] docs: update more examples
---
README.md | 31 ++++++++++++++++---------------
examples/interrupt/main.go | 5 ++---
examples/killafter/main.go | 4 ++--
examples/pdeathsig/main.go | 2 +-
examples/send/main.go | 4 ++--
examples/setpgid/main.go | 2 +-
examples/setsid/main.go | 2 +-
examples/terminate/main.go | 4 ++--
examples/wait/main.go | 5 +++--
execx.go | 21 +++++++++++----------
10 files changed, 41 insertions(+), 39 deletions(-)
diff --git a/README.md b/README.md
index 3e745f7..59a30f8 100644
--- a/README.md
+++ b/README.md
@@ -494,7 +494,7 @@ fmt.Println(out)
### CreationFlags
-CreationFlags is a no-op on non-Windows platforms.
+CreationFlags sets Windows creation flags.
_Example: creation flags_
@@ -512,7 +512,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
### HideWindow
-HideWindow is a no-op on non-Windows platforms.
+HideWindow controls window visibility and sets CREATE_NO_WINDOW for console apps.
_Example: hide window_
@@ -530,7 +530,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
### Pdeathsig
-Pdeathsig sets a parent-death signal on Linux.
+Pdeathsig is a no-op on Windows.
_Example: pdeathsig_
@@ -555,7 +555,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
### Setpgid
-Setpgid sets the process group ID behavior.
+Setpgid is a no-op on Windows.
_Example: setpgid_
@@ -580,7 +580,7 @@ fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
### Setsid
-Setsid sets the session ID behavior.
+Setsid is a no-op on Windows.
_Example: setsid_
@@ -680,8 +680,8 @@ Interrupt sends an interrupt signal to the process.
proc := execx.Command("sleep", "2").Start()
_ = proc.Interrupt()
res, _ := proc.Wait()
-fmt.Println(res.IsSignal(os.Interrupt))
-// #bool true
+fmt.Printf("%+v", res)
+// {Stdout: Stderr: ExitCode:-1 Err: Duration:75.987ms signal:interrupt}
```
### KillAfter
@@ -692,8 +692,8 @@ KillAfter terminates the process after the given duration.
proc := execx.Command("sleep", "2").Start()
proc.KillAfter(100 * time.Millisecond)
res, _ := proc.Wait()
-fmt.Println(res.ExitCode != 0)
-// #bool true
+fmt.Printf("%+v", res)
+// {Stdout: Stderr: ExitCode:-1 Err: Duration:100.456ms signal:killed}
```
### Send
@@ -704,8 +704,8 @@ Send sends a signal to the process.
proc := execx.Command("sleep", "2").Start()
_ = proc.Send(os.Interrupt)
res, _ := proc.Wait()
-fmt.Println(res.IsSignal(os.Interrupt))
-// #bool true
+fmt.Printf("%+v", res)
+// {Stdout: Stderr: ExitCode:-1 Err: Duration:80.123ms signal:interrupt}
```
### Terminate
@@ -716,8 +716,8 @@ Terminate kills the process immediately.
proc := execx.Command("sleep", "2").Start()
_ = proc.Terminate()
res, _ := proc.Wait()
-fmt.Println(res.ExitCode != 0)
-// #bool true
+fmt.Printf("%+v", res)
+// {Stdout: Stderr: ExitCode:-1 Err: Duration:70.654ms signal:killed}
```
### Wait
@@ -727,8 +727,9 @@ 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.Println(res.ExitCode == 0)
-// #bool true
+fmt.Printf("%+v", res)
+// {Stdout:darwin
+// Stderr: ExitCode:0 Err: Duration:1.234ms signal:}
```
## Results
diff --git a/examples/interrupt/main.go b/examples/interrupt/main.go
index 81a4368..e5b5f72 100644
--- a/examples/interrupt/main.go
+++ b/examples/interrupt/main.go
@@ -6,7 +6,6 @@ package main
import (
"fmt"
"github.com/goforj/execx"
- "os"
)
func main() {
@@ -16,6 +15,6 @@ func main() {
proc := execx.Command("sleep", "2").Start()
_ = proc.Interrupt()
res, _ := proc.Wait()
- fmt.Println(res.IsSignal(os.Interrupt))
- // #bool true
+ fmt.Printf("%+v", res)
+ // {Stdout: Stderr: ExitCode:-1 Err: Duration:75.987ms signal:interrupt}
}
diff --git a/examples/killafter/main.go b/examples/killafter/main.go
index ceb0723..a84461a 100644
--- a/examples/killafter/main.go
+++ b/examples/killafter/main.go
@@ -16,6 +16,6 @@ func main() {
proc := execx.Command("sleep", "2").Start()
proc.KillAfter(100 * time.Millisecond)
res, _ := proc.Wait()
- fmt.Println(res.ExitCode != 0)
- // #bool true
+ fmt.Printf("%+v", res)
+ // {Stdout: Stderr: ExitCode:-1 Err: Duration:100.456ms signal:killed}
}
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 5e652c2..7fa2301 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Pdeathsig is a no-op on Windows.
+ // Pdeathsig sets a parent-death signal on Linux.
// Example: pdeathsig
fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
diff --git a/examples/send/main.go b/examples/send/main.go
index de7e060..f262c14 100644
--- a/examples/send/main.go
+++ b/examples/send/main.go
@@ -16,6 +16,6 @@ func main() {
proc := execx.Command("sleep", "2").Start()
_ = proc.Send(os.Interrupt)
res, _ := proc.Wait()
- fmt.Println(res.IsSignal(os.Interrupt))
- // #bool true
+ fmt.Printf("%+v", res)
+ // {Stdout: Stderr: ExitCode:-1 Err: Duration:80.123ms signal:interrupt}
}
diff --git a/examples/setpgid/main.go b/examples/setpgid/main.go
index 7b3c63c..fac2709 100644
--- a/examples/setpgid/main.go
+++ b/examples/setpgid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setpgid is a no-op on Windows.
+ // Setpgid sets the process group ID behavior.
// Example: setpgid
fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
diff --git a/examples/setsid/main.go b/examples/setsid/main.go
index 3f1451a..4ee527b 100644
--- a/examples/setsid/main.go
+++ b/examples/setsid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setsid is a no-op on Windows.
+ // Setsid sets the session ID behavior.
// Example: setsid
fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
diff --git a/examples/terminate/main.go b/examples/terminate/main.go
index 2efba30..fcf624b 100644
--- a/examples/terminate/main.go
+++ b/examples/terminate/main.go
@@ -15,6 +15,6 @@ func main() {
proc := execx.Command("sleep", "2").Start()
_ = proc.Terminate()
res, _ := proc.Wait()
- fmt.Println(res.ExitCode != 0)
- // #bool true
+ fmt.Printf("%+v", res)
+ // {Stdout: Stderr: ExitCode:-1 Err: Duration:70.654ms signal:killed}
}
diff --git a/examples/wait/main.go b/examples/wait/main.go
index b3d1f03..cf79731 100644
--- a/examples/wait/main.go
+++ b/examples/wait/main.go
@@ -14,6 +14,7 @@ func main() {
// Example: wait
proc := execx.Command("go", "env", "GOOS").Start()
res, _ := proc.Wait()
- fmt.Println(res.ExitCode == 0)
- // #bool true
+ fmt.Printf("%+v", res)
+ // {Stdout:darwin
+ // Stderr: ExitCode:0 Err: Duration:1.234ms signal:}
}
diff --git a/execx.go b/execx.go
index 13e8c89..23aa511 100644
--- a/execx.go
+++ b/execx.go
@@ -795,8 +795,9 @@ type Process struct {
//
// proc := execx.Command("go", "env", "GOOS").Start()
// res, _ := proc.Wait()
-// fmt.Println(res.ExitCode == 0)
-// // #bool true
+// fmt.Printf("%+v", res)
+// // {Stdout:darwin
+// // Stderr: ExitCode:0 Err: Duration:1.234ms signal:}
func (p *Process) Wait() (Result, error) {
<-p.done
return p.result, p.result.Err
@@ -810,8 +811,8 @@ func (p *Process) Wait() (Result, error) {
// proc := execx.Command("sleep", "2").Start()
// proc.KillAfter(100 * time.Millisecond)
// res, _ := proc.Wait()
-// fmt.Println(res.ExitCode != 0)
-// // #bool true
+// fmt.Printf("%+v", res)
+// // {Stdout: Stderr: ExitCode:-1 Err: Duration:100.456ms signal:killed}
func (p *Process) KillAfter(d time.Duration) {
p.mu.Lock()
if p.killTimer != nil {
@@ -831,8 +832,8 @@ func (p *Process) KillAfter(d time.Duration) {
// proc := execx.Command("sleep", "2").Start()
// _ = proc.Send(os.Interrupt)
// res, _ := proc.Wait()
-// fmt.Println(res.IsSignal(os.Interrupt))
-// // #bool true
+// fmt.Printf("%+v", res)
+// // {Stdout: Stderr: ExitCode:-1 Err: Duration:80.123ms signal:interrupt}
func (p *Process) Send(sig os.Signal) error {
return p.signalAll(func(proc *os.Process) error {
return proc.Signal(sig)
@@ -847,8 +848,8 @@ func (p *Process) Send(sig os.Signal) error {
// proc := execx.Command("sleep", "2").Start()
// _ = proc.Interrupt()
// res, _ := proc.Wait()
-// fmt.Println(res.IsSignal(os.Interrupt))
-// // #bool true
+// fmt.Printf("%+v", res)
+// // {Stdout: Stderr: ExitCode:-1 Err: Duration:75.987ms signal:interrupt}
func (p *Process) Interrupt() error {
return p.Send(os.Interrupt)
}
@@ -861,8 +862,8 @@ func (p *Process) Interrupt() error {
// proc := execx.Command("sleep", "2").Start()
// _ = proc.Terminate()
// res, _ := proc.Wait()
-// fmt.Println(res.ExitCode != 0)
-// // #bool true
+// fmt.Printf("%+v", res)
+// // {Stdout: Stderr: ExitCode:-1 Err: Duration:70.654ms signal:killed}
func (p *Process) Terminate() error {
return p.signalAll(func(proc *os.Process) error {
return proc.Kill()
From 5a6ffad162957332c76ab0fd24858b02febc71d0 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 16:03:13 -0600
Subject: [PATCH 08/11] docs: examples
---
README.md | 103 +++++++--------------------------
examples/creationflags/main.go | 10 ++--
examples/hidewindow/main.go | 10 ++--
examples/pdeathsig/main.go | 14 ++---
examples/setpgid/main.go | 13 ++---
examples/setsid/main.go | 13 ++---
sysproc_linux.go | 21 ++++---
sysproc_nonwindows.go | 14 +++--
sysproc_unix.go | 21 ++++---
sysproc_windows.go | 35 ++++++-----
10 files changed, 95 insertions(+), 159 deletions(-)
diff --git a/README.md b/README.md
index 59a30f8..527e348 100644
--- a/README.md
+++ b/README.md
@@ -492,115 +492,56 @@ fmt.Println(out)
## OS Controls
-### CreationFlags
-
-CreationFlags sets Windows creation flags.
+OS controls map to `syscall.SysProcAttr` for process/session configuration. Use them when you need process groups, detached sessions, or OS-specific process creation flags. On unsupported platforms they are no-ops.
-_Example: creation flags_
-
-```go
-fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
-// #bool true
-```
+### CreationFlags
-_Example: creation flags_
+CreationFlags sets Windows process creation flags (for example, create a new process group). It is a no-op on non-Windows platforms.
```go
-fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
-// #bool true
+out, _ := execx.Command("printf", "ok").CreationFlags(0x00000200).Output()
+fmt.Print(out)
+// ok
```
### HideWindow
-HideWindow controls window visibility and sets CREATE_NO_WINDOW for console apps.
-
-_Example: hide window_
-
-```go
-fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
-// #bool true
-```
-
-_Example: hide window_
+HideWindow hides console windows on Windows (it sets `SysProcAttr.HideWindow` and `CREATE_NO_WINDOW`). It is a no-op on non-Windows platforms.
```go
-fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
-// #bool true
+out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
+fmt.Print(out)
+// ok
```
### Pdeathsig
-Pdeathsig is a no-op on Windows.
-
-_Example: pdeathsig_
+Pdeathsig sets a parent-death signal on Linux so the child receives a signal if the parent exits. It is a no-op on non-Linux platforms.
```go
-fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
-// #bool true
-```
-
-_Example: pdeathsig_
-
-```go
-fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
-// #bool true
-```
-
-_Example: pdeathsig_
-
-```go
-fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
-// #bool true
+out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
+fmt.Print(out)
+// ok
```
### Setpgid
-Setpgid is a no-op on Windows.
-
-_Example: setpgid_
-
-```go
-fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
-// #bool true
-```
-
-_Example: setpgid_
+Setpgid places the child in a new process group. Use this when you want to signal or terminate a group independently of the parent.
```go
-fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
-// #bool true
-```
-
-_Example: setpgid_
-
-```go
-fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
-// #bool true
+out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
+fmt.Print(out)
+// ok
```
### Setsid
-Setsid is a no-op on Windows.
-
-_Example: setsid_
-
-```go
-fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
-// #bool true
-```
-
-_Example: setsid_
+Setsid starts the child in a new session, detaching it from the controlling terminal.
```go
-fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
-// #bool true
-```
-
-_Example: setsid_
-
-```go
-fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
-// #bool true
+out, _ := execx.Command("printf", "ok").Setsid(true).Output()
+fmt.Print(out)
+// ok
```
## Pipelining
diff --git a/examples/creationflags/main.go b/examples/creationflags/main.go
index fbcee1e..ee8fdba 100644
--- a/examples/creationflags/main.go
+++ b/examples/creationflags/main.go
@@ -9,12 +9,10 @@ import (
)
func main() {
- // CreationFlags sets Windows creation flags.
+ // CreationFlags sets Windows process creation flags (for example, create a new process group).
// Example: creation flags
- fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
- // #bool true
- // Example: creation flags
- fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
- // #bool true
+ out, _ := execx.Command("printf", "ok").CreationFlags(0x00000200).Output()
+ fmt.Print(out)
+ // ok
}
diff --git a/examples/hidewindow/main.go b/examples/hidewindow/main.go
index 7de6212..2ca4166 100644
--- a/examples/hidewindow/main.go
+++ b/examples/hidewindow/main.go
@@ -9,12 +9,10 @@ import (
)
func main() {
- // HideWindow controls window visibility and sets CREATE_NO_WINDOW for console apps.
+ // HideWindow hides console windows and sets CREATE_NO_WINDOW for console apps.
// Example: hide window
- fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
- // #bool true
- // Example: hide window
- fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
- // #bool true
+ out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
+ fmt.Print(out)
+ // ok
}
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 7fa2301..04a4d12 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -6,18 +6,14 @@ package main
import (
"fmt"
"github.com/goforj/execx"
+ "syscall"
)
func main() {
- // Pdeathsig sets a parent-death signal on Linux.
+ // Pdeathsig is a no-op on Windows; on Linux it signals the child when the parent exits.
// Example: pdeathsig
- fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
- // #bool true
- // Example: pdeathsig
- fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
- // #bool true
- // Example: pdeathsig
- fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
- // #bool true
+ out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
+ fmt.Print(out)
+ // ok
}
diff --git a/examples/setpgid/main.go b/examples/setpgid/main.go
index fac2709..774652e 100644
--- a/examples/setpgid/main.go
+++ b/examples/setpgid/main.go
@@ -9,15 +9,10 @@ import (
)
func main() {
- // Setpgid sets the process group ID behavior.
+ // Setpgid is a no-op on Windows; on Unix it places the child in a new process group.
// Example: setpgid
- fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
- // #bool true
- // Example: setpgid
- fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
- // #bool true
- // Example: setpgid
- fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
- // #bool true
+ out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
+ fmt.Print(out)
+ // ok
}
diff --git a/examples/setsid/main.go b/examples/setsid/main.go
index 4ee527b..1311a04 100644
--- a/examples/setsid/main.go
+++ b/examples/setsid/main.go
@@ -9,15 +9,10 @@ import (
)
func main() {
- // Setsid sets the session ID behavior.
+ // Setsid is a no-op on Windows; on Unix it starts a new session.
// Example: setsid
- fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
- // #bool true
- // Example: setsid
- fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
- // #bool true
- // Example: setsid
- fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
- // #bool true
+ out, _ := execx.Command("printf", "ok").Setsid(true).Output()
+ fmt.Print(out)
+ // ok
}
diff --git a/sysproc_linux.go b/sysproc_linux.go
index 3fe434f..9b2ca29 100644
--- a/sysproc_linux.go
+++ b/sysproc_linux.go
@@ -4,39 +4,42 @@ package execx
import "syscall"
-// Setpgid sets the process group ID behavior.
+// Setpgid places the child in a new process group for group signals.
// @group OS Controls
//
// Example: setpgid
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Setpgid(on bool) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.Setpgid = on
return c
}
-// Setsid sets the session ID behavior.
+// Setsid starts the child in a new session, detaching it from the terminal.
// @group OS Controls
//
// Example: setsid
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Setsid(true).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Setsid(on bool) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.Setsid = on
return c
}
-// Pdeathsig sets a parent-death signal on Linux.
+// Pdeathsig sets a parent-death signal on Linux so the child is signaled if the parent exits.
// @group OS Controls
//
// Example: pdeathsig
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Pdeathsig(sig syscall.Signal) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.Pdeathsig = sig
diff --git a/sysproc_nonwindows.go b/sysproc_nonwindows.go
index dc0f306..05e5417 100644
--- a/sysproc_nonwindows.go
+++ b/sysproc_nonwindows.go
@@ -2,24 +2,26 @@
package execx
-// CreationFlags is a no-op on non-Windows platforms.
+// CreationFlags is a no-op on non-Windows platforms; on Windows it sets process creation flags.
// @group OS Controls
//
// Example: creation flags
//
-// fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").CreationFlags(0x00000200).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) CreationFlags(_ uint32) *Cmd {
return c
}
-// HideWindow is a no-op on non-Windows platforms.
+// HideWindow is a no-op on non-Windows platforms; on Windows it hides console windows.
// @group OS Controls
//
// Example: hide window
//
-// fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) HideWindow(_ bool) *Cmd {
return c
}
diff --git a/sysproc_unix.go b/sysproc_unix.go
index 087c38b..15b2478 100644
--- a/sysproc_unix.go
+++ b/sysproc_unix.go
@@ -4,39 +4,42 @@ package execx
import "syscall"
-// Setpgid sets the process group ID behavior.
+// Setpgid places the child in a new process group for group signals.
// @group OS Controls
//
// Example: setpgid
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Setpgid(on bool) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.Setpgid = on
return c
}
-// Setsid sets the session ID behavior.
+// Setsid starts the child in a new session, detaching it from the terminal.
// @group OS Controls
//
// Example: setsid
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Setsid(true).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Setsid(on bool) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.Setsid = on
return c
}
-// Pdeathsig is a no-op on non-Linux Unix platforms.
+// Pdeathsig is a no-op on non-Linux Unix platforms; on Linux it signals the child when the parent exits.
// @group OS Controls
//
// Example: pdeathsig
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Pdeathsig(_ syscall.Signal) *Cmd {
return c
}
diff --git a/sysproc_windows.go b/sysproc_windows.go
index c84abb3..8ed9f7e 100644
--- a/sysproc_windows.go
+++ b/sysproc_windows.go
@@ -4,59 +4,64 @@ package execx
import "syscall"
-// Setpgid is a no-op on Windows.
+// Setpgid is a no-op on Windows; on Unix it places the child in a new process group.
// @group OS Controls
//
// Example: setpgid
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Setpgid(true) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Setpgid(_ bool) *Cmd {
return c
}
-// Setsid is a no-op on Windows.
+// Setsid is a no-op on Windows; on Unix it starts a new session.
// @group OS Controls
//
// Example: setsid
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Setsid(true) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Setsid(true).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Setsid(_ bool) *Cmd {
return c
}
-// Pdeathsig is a no-op on Windows.
+// Pdeathsig is a no-op on Windows; on Linux it signals the child when the parent exits.
// @group OS Controls
//
// Example: pdeathsig
//
-// fmt.Println(execx.Command("go", "env", "GOOS").Pdeathsig(0) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) Pdeathsig(_ syscall.Signal) *Cmd {
return c
}
-// CreationFlags sets Windows creation flags.
+// CreationFlags sets Windows process creation flags (for example, create a new process group).
// @group OS Controls
//
// Example: creation flags
//
-// fmt.Println(execx.Command("go", "env", "GOOS").CreationFlags(0) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").CreationFlags(0x00000200).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) CreationFlags(flags uint32) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.CreationFlags = flags
return c
}
-// HideWindow controls window visibility and sets CREATE_NO_WINDOW for console apps.
+// HideWindow hides console windows and sets CREATE_NO_WINDOW for console apps.
// @group OS Controls
//
// Example: hide window
//
-// fmt.Println(execx.Command("go", "env", "GOOS").HideWindow(true) != nil)
-// // #bool true
+// out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
+// fmt.Print(out)
+// // ok
func (c *Cmd) HideWindow(on bool) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.HideWindow = on
From 888c54ff14f78aacefffe49e64328128ffd0266c Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 16:10:25 -0600
Subject: [PATCH 09/11] docs: example clarifications
---
README.md | 8 +++++++-
creationflags_nonwindows.go | 12 ++++++++++++
creationflags_windows.go | 14 ++++++++++++++
examples/creationflags/main.go | 4 ++--
examples/hidewindow/main.go | 2 +-
examples/pdeathsig/main.go | 3 +--
examples/setpgid/main.go | 2 +-
examples/setsid/main.go | 2 +-
sysproc_nonwindows.go | 12 ------------
sysproc_unix.go | 18 ------------------
sysproc_windows.go | 22 +++-------------------
11 files changed, 42 insertions(+), 57 deletions(-)
create mode 100644 creationflags_nonwindows.go
create mode 100644 creationflags_windows.go
diff --git a/README.md b/README.md
index 527e348..a9d8456 100644
--- a/README.md
+++ b/README.md
@@ -498,8 +498,14 @@ OS controls map to `syscall.SysProcAttr` for process/session configuration. Use
CreationFlags sets Windows process creation flags (for example, create a new process group). It is a no-op on non-Windows platforms.
+Common flags:
+
+- `execx.CreateNewProcessGroup`
+- `execx.CreateNewConsole`
+- `execx.CreateNoWindow`
+
```go
-out, _ := execx.Command("printf", "ok").CreationFlags(0x00000200).Output()
+out, _ := execx.Command("printf", "ok").CreationFlags(execx.CreateNewProcessGroup).Output()
fmt.Print(out)
// ok
```
diff --git a/creationflags_nonwindows.go b/creationflags_nonwindows.go
new file mode 100644
index 0000000..b36b6f0
--- /dev/null
+++ b/creationflags_nonwindows.go
@@ -0,0 +1,12 @@
+//go:build !windows
+
+package execx
+
+const (
+ // CreateNewProcessGroup starts the process in a new process group.
+ CreateNewProcessGroup = 0x00000200
+ // CreateNewConsole creates a new console for the process.
+ CreateNewConsole = 0x00000010
+ // CreateNoWindow prevents console windows from being created.
+ CreateNoWindow = 0x08000000
+)
diff --git a/creationflags_windows.go b/creationflags_windows.go
new file mode 100644
index 0000000..e255a80
--- /dev/null
+++ b/creationflags_windows.go
@@ -0,0 +1,14 @@
+//go:build windows
+
+package execx
+
+import "syscall"
+
+const (
+ // CreateNewProcessGroup starts the process in a new process group.
+ CreateNewProcessGroup = syscall.CREATE_NEW_PROCESS_GROUP
+ // CreateNewConsole creates a new console for the process.
+ CreateNewConsole = syscall.CREATE_NEW_CONSOLE
+ // CreateNoWindow prevents console windows from being created.
+ CreateNoWindow = syscall.CREATE_NO_WINDOW
+)
diff --git a/examples/creationflags/main.go b/examples/creationflags/main.go
index ee8fdba..3876d16 100644
--- a/examples/creationflags/main.go
+++ b/examples/creationflags/main.go
@@ -9,10 +9,10 @@ import (
)
func main() {
- // CreationFlags sets Windows process creation flags (for example, create a new process group).
+ // CreationFlags is a no-op on non-Windows platforms; on Windows it sets process creation flags.
// Example: creation flags
- out, _ := execx.Command("printf", "ok").CreationFlags(0x00000200).Output()
+ out, _ := execx.Command("printf", "ok").CreationFlags(execx.CreateNewProcessGroup).Output()
fmt.Print(out)
// ok
}
diff --git a/examples/hidewindow/main.go b/examples/hidewindow/main.go
index 2ca4166..1f88f6c 100644
--- a/examples/hidewindow/main.go
+++ b/examples/hidewindow/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // HideWindow hides console windows and sets CREATE_NO_WINDOW for console apps.
+ // HideWindow is a no-op on non-Windows platforms; on Windows it hides console windows.
// Example: hide window
out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 04a4d12..ae92a25 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -6,11 +6,10 @@ package main
import (
"fmt"
"github.com/goforj/execx"
- "syscall"
)
func main() {
- // Pdeathsig is a no-op on Windows; on Linux it signals the child when the parent exits.
+ // Pdeathsig sets a parent-death signal on Linux so the child is signaled if the parent exits.
// Example: pdeathsig
out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
diff --git a/examples/setpgid/main.go b/examples/setpgid/main.go
index 774652e..8e02196 100644
--- a/examples/setpgid/main.go
+++ b/examples/setpgid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setpgid is a no-op on Windows; on Unix it places the child in a new process group.
+ // Setpgid places the child in a new process group for group signals.
// Example: setpgid
out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
diff --git a/examples/setsid/main.go b/examples/setsid/main.go
index 1311a04..548a68e 100644
--- a/examples/setsid/main.go
+++ b/examples/setsid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setsid is a no-op on Windows; on Unix it starts a new session.
+ // Setsid starts the child in a new session, detaching it from the terminal.
// Example: setsid
out, _ := execx.Command("printf", "ok").Setsid(true).Output()
diff --git a/sysproc_nonwindows.go b/sysproc_nonwindows.go
index 05e5417..1391b59 100644
--- a/sysproc_nonwindows.go
+++ b/sysproc_nonwindows.go
@@ -4,24 +4,12 @@ package execx
// CreationFlags is a no-op on non-Windows platforms; on Windows it sets process creation flags.
// @group OS Controls
-//
-// Example: creation flags
-//
-// out, _ := execx.Command("printf", "ok").CreationFlags(0x00000200).Output()
-// fmt.Print(out)
-// // ok
func (c *Cmd) CreationFlags(_ uint32) *Cmd {
return c
}
// HideWindow is a no-op on non-Windows platforms; on Windows it hides console windows.
// @group OS Controls
-//
-// Example: hide window
-//
-// out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
-// fmt.Print(out)
-// // ok
func (c *Cmd) HideWindow(_ bool) *Cmd {
return c
}
diff --git a/sysproc_unix.go b/sysproc_unix.go
index 15b2478..bb45185 100644
--- a/sysproc_unix.go
+++ b/sysproc_unix.go
@@ -6,12 +6,6 @@ import "syscall"
// Setpgid places the child in a new process group for group signals.
// @group OS Controls
-//
-// Example: setpgid
-//
-// out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
-// fmt.Print(out)
-// // ok
func (c *Cmd) Setpgid(on bool) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.Setpgid = on
@@ -20,12 +14,6 @@ func (c *Cmd) Setpgid(on bool) *Cmd {
// Setsid starts the child in a new session, detaching it from the terminal.
// @group OS Controls
-//
-// Example: setsid
-//
-// out, _ := execx.Command("printf", "ok").Setsid(true).Output()
-// fmt.Print(out)
-// // ok
func (c *Cmd) Setsid(on bool) *Cmd {
c.ensureSysProcAttr()
c.sysProcAttr.Setsid = on
@@ -34,12 +22,6 @@ func (c *Cmd) Setsid(on bool) *Cmd {
// Pdeathsig is a no-op on non-Linux Unix platforms; on Linux it signals the child when the parent exits.
// @group OS Controls
-//
-// Example: pdeathsig
-//
-// out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
-// fmt.Print(out)
-// // ok
func (c *Cmd) Pdeathsig(_ syscall.Signal) *Cmd {
return c
}
diff --git a/sysproc_windows.go b/sysproc_windows.go
index 8ed9f7e..e6d999b 100644
--- a/sysproc_windows.go
+++ b/sysproc_windows.go
@@ -6,36 +6,18 @@ import "syscall"
// Setpgid is a no-op on Windows; on Unix it places the child in a new process group.
// @group OS Controls
-//
-// Example: setpgid
-//
-// out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
-// fmt.Print(out)
-// // ok
func (c *Cmd) Setpgid(_ bool) *Cmd {
return c
}
// Setsid is a no-op on Windows; on Unix it starts a new session.
// @group OS Controls
-//
-// Example: setsid
-//
-// out, _ := execx.Command("printf", "ok").Setsid(true).Output()
-// fmt.Print(out)
-// // ok
func (c *Cmd) Setsid(_ bool) *Cmd {
return c
}
// Pdeathsig is a no-op on Windows; on Linux it signals the child when the parent exits.
// @group OS Controls
-//
-// Example: pdeathsig
-//
-// out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
-// fmt.Print(out)
-// // ok
func (c *Cmd) Pdeathsig(_ syscall.Signal) *Cmd {
return c
}
@@ -43,9 +25,11 @@ func (c *Cmd) Pdeathsig(_ syscall.Signal) *Cmd {
// CreationFlags sets Windows process creation flags (for example, create a new process group).
// @group OS Controls
//
+// Common flags: execx.CreateNewProcessGroup, execx.CreateNewConsole, execx.CreateNoWindow.
+//
// Example: creation flags
//
-// out, _ := execx.Command("printf", "ok").CreationFlags(0x00000200).Output()
+// out, _ := execx.Command("printf", "ok").CreationFlags(execx.CreateNewProcessGroup).Output()
// fmt.Print(out)
// // ok
func (c *Cmd) CreationFlags(flags uint32) *Cmd {
From b4f3a637ff50940246aaf8ca33382b14a1a72b44 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 16:32:57 -0600
Subject: [PATCH 10/11] feat: shadow printing
---
README.md | 59 ++++++++
examples/pdeathsig/main.go | 2 +-
examples/setpgid/main.go | 2 +-
examples/setsid/main.go | 2 +-
examples/shadowprint/main.go | 15 ++
examples/shadowprintformatter/main.go | 21 +++
examples/shadowprintmask/main.go | 21 +++
examples/shadowprintoff/main.go | 13 ++
examples/shadowprintprefix/main.go | 15 ++
execx.go | 199 ++++++++++++++++++++++++++
shadow_colors.go | 8 ++
11 files changed, 354 insertions(+), 3 deletions(-)
create mode 100644 examples/shadowprint/main.go
create mode 100644 examples/shadowprintformatter/main.go
create mode 100644 examples/shadowprintmask/main.go
create mode 100644 examples/shadowprintoff/main.go
create mode 100644 examples/shadowprintprefix/main.go
create mode 100644 shadow_colors.go
diff --git a/README.md b/README.md
index a9d8456..9c2b78f 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,8 @@ fmt.Println(res.ExitCode == 0)
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
@@ -202,6 +204,7 @@ All public APIs are covered by runnable examples under `./examples`, and the tes
| **Results** | [IsExitCode](#isexitcode) [IsSignal](#issignal) [OK](#ok) |
| **Streaming** | [OnStderr](#onstderr) [OnStdout](#onstdout) [StderrWriter](#stderrwriter) [StdoutWriter](#stdoutwriter) |
| **WorkingDir** | [Dir](#dir) |
+| **User Feedback** | [ShadowPrint](#shadowprint) [ShadowPrintOff](#shadowprintoff) [ShadowPrintPrefix](#shadowprintprefix) [ShadowPrintMask](#shadowprintmask) [ShadowPrintFormatter](#shadowprintformatter) |
## Arguments
@@ -286,6 +289,62 @@ fmt.Println(cmd.ShellEscaped())
// #string echo 'hello world' 'it'\\''s'
```
+## User Feedback
+
+### ShadowPrint
+
+ShadowPrint writes the shell-escaped command to stderr before and after execution.
+
+```go
+_, _ = execx.Command("printf", "hi").ShadowPrint().Run()
+// execx > printf hi
+// execx > printf hi (1ms)
+```
+
+### ShadowPrintOff
+
+ShadowPrintOff disables shadow printing for this command chain.
+
+```go
+_, _ = execx.Command("printf", "hi").ShadowPrint().ShadowPrintOff().Run()
+```
+
+### ShadowPrintPrefix
+
+ShadowPrintPrefix sets the prefix used by ShadowPrint.
+
+```go
+_, _ = execx.Command("printf", "hi").ShadowPrintPrefix("run").Run()
+// run > printf hi
+// run > printf hi (1ms)
+```
+
+### ShadowPrintMask
+
+ShadowPrintMask applies a masker to the shadow-printed command string.
+
+```go
+mask := func(cmd string) string {
+ return strings.ReplaceAll(cmd, "secret", "***")
+}
+_, _ = execx.Command("printf", "secret").ShadowPrintMask(mask).Run()
+// execx > printf ***
+// execx > printf *** (1ms)
+```
+
+### ShadowPrintFormatter
+
+ShadowPrintFormatter 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").ShadowPrintFormatter(formatter).Run()
+// shadow: before printf hi
+// shadow: after printf hi
+```
+
### String
String returns a human-readable representation of the command.
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index ae92a25..77c62ef 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Pdeathsig sets a parent-death signal on Linux so the child is signaled if the parent exits.
+ // Pdeathsig is a no-op on Windows; on Linux it signals the child when the parent exits.
// Example: pdeathsig
out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
diff --git a/examples/setpgid/main.go b/examples/setpgid/main.go
index 8e02196..774652e 100644
--- a/examples/setpgid/main.go
+++ b/examples/setpgid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setpgid places the child in a new process group for group signals.
+ // Setpgid is a no-op on Windows; on Unix it places the child in a new process group.
// Example: setpgid
out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
diff --git a/examples/setsid/main.go b/examples/setsid/main.go
index 548a68e..1311a04 100644
--- a/examples/setsid/main.go
+++ b/examples/setsid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setsid starts the child in a new session, detaching it from the terminal.
+ // Setsid is a no-op on Windows; on Unix it starts a new session.
// Example: setsid
out, _ := execx.Command("printf", "ok").Setsid(true).Output()
diff --git a/examples/shadowprint/main.go b/examples/shadowprint/main.go
new file mode 100644
index 0000000..0499d1e
--- /dev/null
+++ b/examples/shadowprint/main.go
@@ -0,0 +1,15 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import "github.com/goforj/execx"
+
+func main() {
+ // ShadowPrint writes the shell-escaped command to stderr before and after execution.
+
+ // Example: shadow print
+ _, _ = execx.Command("printf", "hi").ShadowPrint().Run()
+ // execx > printf hi
+ // execx > printf hi (1ms)
+}
diff --git a/examples/shadowprintformatter/main.go b/examples/shadowprintformatter/main.go
new file mode 100644
index 0000000..571baa4
--- /dev/null
+++ b/examples/shadowprintformatter/main.go
@@ -0,0 +1,21 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // ShadowPrintFormatter sets a formatter for ShadowPrint output.
+
+ // Example: shadow print formatter
+ formatter := func(ev execx.ShadowEvent) string {
+ return fmt.Sprintf("shadow: %s %s", ev.Phase, ev.Command)
+ }
+ _, _ = execx.Command("printf", "hi").ShadowPrintFormatter(formatter).Run()
+ // shadow: before printf hi
+ // shadow: after printf hi
+}
diff --git a/examples/shadowprintmask/main.go b/examples/shadowprintmask/main.go
new file mode 100644
index 0000000..94c775a
--- /dev/null
+++ b/examples/shadowprintmask/main.go
@@ -0,0 +1,21 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "github.com/goforj/execx"
+ "strings"
+)
+
+func main() {
+ // ShadowPrintMask sets a command masker for ShadowPrint output.
+
+ // Example: shadow print mask
+ mask := func(cmd string) string {
+ return strings.ReplaceAll(cmd, "secret", "***")
+ }
+ _, _ = execx.Command("printf", "secret").ShadowPrintMask(mask).Run()
+ // execx > printf ***
+ // execx > printf *** (1ms)
+}
diff --git a/examples/shadowprintoff/main.go b/examples/shadowprintoff/main.go
new file mode 100644
index 0000000..42bfad6
--- /dev/null
+++ b/examples/shadowprintoff/main.go
@@ -0,0 +1,13 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import "github.com/goforj/execx"
+
+func main() {
+ // ShadowPrintOff disables shadow printing for this command chain.
+
+ // Example: shadow print off
+ _, _ = execx.Command("printf", "hi").ShadowPrint().ShadowPrintOff().Run()
+}
diff --git a/examples/shadowprintprefix/main.go b/examples/shadowprintprefix/main.go
new file mode 100644
index 0000000..1b1cde3
--- /dev/null
+++ b/examples/shadowprintprefix/main.go
@@ -0,0 +1,15 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import "github.com/goforj/execx"
+
+func main() {
+ // ShadowPrintPrefix sets the prefix used by ShadowPrint.
+
+ // Example: shadow print prefix
+ _, _ = execx.Command("printf", "hi").ShadowPrintPrefix("run").Run()
+ // run > printf hi
+ // run > printf hi (1ms)
+}
diff --git a/execx.go b/execx.go
index 23aa511..a3efcf1 100644
--- a/execx.go
+++ b/execx.go
@@ -74,6 +74,11 @@ type Cmd struct {
next *Cmd
root *Cmd
pipeMode pipeMode
+
+ shadowPrint bool
+ shadowPrefix string
+ shadowFormatter func(ShadowEvent) string
+ shadowMask func(string) string
}
// Arg appends arguments to the command.
@@ -527,6 +532,82 @@ func (c *Cmd) ShellEscaped() string {
return strings.Join(parts, " ")
}
+// ShadowPrint writes the shell-escaped command to stderr before and after execution.
+// @group User Feedback
+//
+// Example: shadow print
+//
+// _, _ = execx.Command("printf", "hi").ShadowPrint().Run()
+// // execx > printf hi
+// // execx > printf hi (1ms)
+func (c *Cmd) ShadowPrint() *Cmd {
+ c.rootCmd().shadowPrint = true
+ return c
+}
+
+// ShadowPrintOff disables shadow printing for this command chain.
+// @group User Feedback
+//
+// Example: shadow print off
+//
+// _, _ = execx.Command("printf", "hi").ShadowPrint().ShadowPrintOff().Run()
+func (c *Cmd) ShadowPrintOff() *Cmd {
+ root := c.rootCmd()
+ root.shadowPrint = false
+ return c
+}
+
+// ShadowPrintPrefix sets the prefix used by ShadowPrint.
+// @group User Feedback
+//
+// Example: shadow print prefix
+//
+// _, _ = execx.Command("printf", "hi").ShadowPrintPrefix("run").Run()
+// // run > printf hi
+// // run > printf hi (1ms)
+func (c *Cmd) ShadowPrintPrefix(prefix string) *Cmd {
+ root := c.rootCmd()
+ root.shadowPrint = true
+ root.shadowPrefix = prefix
+ return c
+}
+
+// ShadowPrintMask sets a command masker for ShadowPrint output.
+// @group User Feedback
+//
+// Example: shadow print mask
+//
+// mask := func(cmd string) string {
+// return strings.ReplaceAll(cmd, "secret", "***")
+// }
+// _, _ = execx.Command("printf", "secret").ShadowPrintMask(mask).Run()
+// // execx > printf ***
+// // execx > printf *** (1ms)
+func (c *Cmd) ShadowPrintMask(fn func(string) string) *Cmd {
+ root := c.rootCmd()
+ root.shadowPrint = true
+ root.shadowMask = fn
+ return c
+}
+
+// ShadowPrintFormatter sets a formatter for ShadowPrint output.
+// @group User Feedback
+//
+// Example: shadow print formatter
+//
+// formatter := func(ev execx.ShadowEvent) string {
+// return fmt.Sprintf("shadow: %s %s", ev.Phase, ev.Command)
+// }
+// _, _ = execx.Command("printf", "hi").ShadowPrintFormatter(formatter).Run()
+// // shadow: before printf hi
+// // shadow: after printf hi
+func (c *Cmd) ShadowPrintFormatter(fn func(ShadowEvent) string) *Cmd {
+ root := c.rootCmd()
+ root.shadowPrint = true
+ root.shadowFormatter = fn
+ return c
+}
+
// Run executes the command and returns the result and any error.
// @group Execution
//
@@ -536,10 +617,12 @@ func (c *Cmd) ShellEscaped() string {
// fmt.Println(res.ExitCode == 0)
// // #bool true
func (c *Cmd) Run() (Result, error) {
+ shadow := c.shadowPrintStart(false)
pipe := c.newPipeline(false)
pipe.start()
pipe.wait()
result, _ := pipe.primaryResult(c.rootCmd().pipeMode)
+ shadow.finish()
return result, result.Err
}
@@ -595,10 +678,12 @@ func (c *Cmd) OutputTrimmed() (string, error) {
// // Run 'go help env' for details.
// // false
func (c *Cmd) CombinedOutput() (string, error) {
+ shadow := c.shadowPrintStart(false)
pipe := c.newPipeline(true)
pipe.start()
pipe.wait()
result, combined := pipe.primaryResult(c.rootCmd().pipeMode)
+ shadow.finish()
return combined, result.Err
}
@@ -616,10 +701,12 @@ func (c *Cmd) CombinedOutput() (string, error) {
// // {Stdout:GO Stderr: ExitCode:0 Err: Duration:4.976291ms signal:}
// // ]
func (c *Cmd) PipelineResults() ([]Result, error) {
+ shadow := c.shadowPrintStart(false)
pipe := c.newPipeline(false)
pipe.start()
pipe.wait()
results := pipe.results()
+ shadow.finish()
return results, firstResultErr(results)
}
@@ -633,6 +720,7 @@ func (c *Cmd) PipelineResults() ([]Result, error) {
// fmt.Println(res.ExitCode == 0)
// // #bool true
func (c *Cmd) Start() *Process {
+ shadow := c.shadowPrintStart(true)
pipe := c.newPipeline(false)
pipe.start()
@@ -640,6 +728,7 @@ func (c *Cmd) Start() *Process {
pipeline: pipe,
mode: c.rootCmd().pipeMode,
done: make(chan struct{}),
+ shadow: shadow,
}
go func() {
pipe.wait()
@@ -776,6 +865,113 @@ func shellEscape(arg string) string {
return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
}
+type shadowContext struct {
+ cmd *Cmd
+ start time.Time
+ async bool
+}
+
+// ShadowPhase describes whether the shadow print is before or after execution.
+type ShadowPhase string
+
+const (
+ ShadowBefore ShadowPhase = "before"
+ ShadowAfter ShadowPhase = "after"
+)
+
+// ShadowEvent captures details for ShadowPrint formatting.
+type ShadowEvent struct {
+ Command string
+ RawCommand string
+ Phase ShadowPhase
+ Duration time.Duration
+ Async bool
+}
+
+func (c *Cmd) shadowPrintStart(async bool) *shadowContext {
+ root := c.rootCmd()
+ if root == nil || !root.shadowPrint {
+ return nil
+ }
+ ctx := &shadowContext{
+ cmd: root,
+ start: time.Now(),
+ async: async,
+ }
+ shadowPrintLine(root, ShadowBefore, 0, async)
+ return ctx
+}
+
+func (c *Cmd) shadowCommand() string {
+ root := c.rootCmd()
+ if root == nil {
+ return ""
+ }
+ parts := []string{}
+ for stage := root; stage != nil; stage = stage.next {
+ parts = append(parts, stage.ShellEscaped())
+ }
+ return strings.Join(parts, " | ")
+}
+
+func (s *shadowContext) finish() {
+ if s == nil || s.cmd == nil {
+ return
+ }
+ duration := time.Since(s.start).Round(time.Millisecond)
+ shadowPrintLine(s.cmd, ShadowAfter, duration, s.async)
+}
+
+func shadowPrintLine(cmd *Cmd, phase ShadowPhase, duration time.Duration, async bool) {
+ if cmd == nil {
+ return
+ }
+ rawCommand := cmd.shadowCommand()
+ commandLine := rawCommand
+ if cmd.shadowMask != nil {
+ commandLine = cmd.shadowMask(commandLine)
+ }
+ event := ShadowEvent{
+ Command: commandLine,
+ RawCommand: rawCommand,
+ Phase: phase,
+ Duration: duration,
+ Async: async,
+ }
+ if cmd.shadowFormatter != nil {
+ line := cmd.shadowFormatter(event)
+ if line != "" {
+ fmt.Fprintln(os.Stderr, line)
+ }
+ return
+ }
+ prefix := cmd.shadowPrefix
+ if prefix == "" {
+ prefix = "execx"
+ }
+ timing := ""
+ if phase == ShadowAfter {
+ timing = " (" + duration.String()
+ if async {
+ timing += ", async"
+ }
+ timing += ")"
+ } else if async {
+ timing = " (async)"
+ }
+ fmt.Fprintf(
+ os.Stderr,
+ "%s%s > %s%s%s%s%s\n",
+ shadowBoldHighIntensityBlack,
+ prefix,
+ shadowReset,
+ shadowHighIntensityBlack,
+ commandLine,
+ shadowFadedGray,
+ timing+shadowReset,
+ )
+}
+
// Process represents an asynchronously running command.
type Process struct {
pipeline *pipeline
@@ -783,6 +979,8 @@ type Process struct {
done chan struct{}
result Result
+ shadow *shadowContext
+
resultOnce sync.Once
mu sync.Mutex
killTimer *time.Timer
@@ -900,6 +1098,7 @@ func (p *Process) GracefulShutdown(sig os.Signal, timeout time.Duration) error {
func (p *Process) finish(result Result) {
p.resultOnce.Do(func() {
p.result = result
+ p.shadow.finish()
close(p.done)
})
}
diff --git a/shadow_colors.go b/shadow_colors.go
new file mode 100644
index 0000000..c8a5e85
--- /dev/null
+++ b/shadow_colors.go
@@ -0,0 +1,8 @@
+package execx
+
+const (
+ shadowBoldHighIntensityBlack = "\033[1;90m"
+ shadowHighIntensityBlack = "\033[90m"
+ shadowFadedGray = "\u001B[38;5;236m"
+ shadowReset = "\033[0m"
+)
From 7ec5ee0b3a712e5297a3924d09b447eb04f8a973 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Tue, 30 Dec 2025 16:45:57 -0600
Subject: [PATCH 11/11] chore: tests
---
examples/creationflags/main.go | 2 +-
examples/hidewindow/main.go | 2 +-
examples/pdeathsig/main.go | 3 +-
examples/setpgid/main.go | 2 +-
examples/setsid/main.go | 2 +-
execx.go | 3 -
execx_shadow_test.go | 143 +++++++++++++++++++++++++++++++++
7 files changed, 149 insertions(+), 8 deletions(-)
create mode 100644 execx_shadow_test.go
diff --git a/examples/creationflags/main.go b/examples/creationflags/main.go
index 3876d16..2556811 100644
--- a/examples/creationflags/main.go
+++ b/examples/creationflags/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // CreationFlags is a no-op on non-Windows platforms; on Windows it sets process creation flags.
+ // CreationFlags sets Windows process creation flags (for example, create a new process group).
// Example: creation flags
out, _ := execx.Command("printf", "ok").CreationFlags(execx.CreateNewProcessGroup).Output()
diff --git a/examples/hidewindow/main.go b/examples/hidewindow/main.go
index 1f88f6c..2ca4166 100644
--- a/examples/hidewindow/main.go
+++ b/examples/hidewindow/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // HideWindow is a no-op on non-Windows platforms; on Windows it hides console windows.
+ // HideWindow hides console windows and sets CREATE_NO_WINDOW for console apps.
// Example: hide window
out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 77c62ef..450a6a6 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -6,10 +6,11 @@ package main
import (
"fmt"
"github.com/goforj/execx"
+ "syscall"
)
func main() {
- // Pdeathsig is a no-op on Windows; on Linux it signals the child when the parent exits.
+ // Pdeathsig sets a parent-death signal on Linux so the child is signaled if the parent exits.
// Example: pdeathsig
out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
diff --git a/examples/setpgid/main.go b/examples/setpgid/main.go
index 774652e..8e02196 100644
--- a/examples/setpgid/main.go
+++ b/examples/setpgid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setpgid is a no-op on Windows; on Unix it places the child in a new process group.
+ // Setpgid places the child in a new process group for group signals.
// Example: setpgid
out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
diff --git a/examples/setsid/main.go b/examples/setsid/main.go
index 1311a04..548a68e 100644
--- a/examples/setsid/main.go
+++ b/examples/setsid/main.go
@@ -9,7 +9,7 @@ import (
)
func main() {
- // Setsid is a no-op on Windows; on Unix it starts a new session.
+ // Setsid starts the child in a new session, detaching it from the terminal.
// Example: setsid
out, _ := execx.Command("printf", "ok").Setsid(true).Output()
diff --git a/execx.go b/execx.go
index a3efcf1..12ea48f 100644
--- a/execx.go
+++ b/execx.go
@@ -904,9 +904,6 @@ func (c *Cmd) shadowPrintStart(async bool) *shadowContext {
func (c *Cmd) shadowCommand() string {
root := c.rootCmd()
- if root == nil {
- return ""
- }
parts := []string{}
for stage := root; stage != nil; stage = stage.next {
parts = append(parts, stage.ShellEscaped())
diff --git a/execx_shadow_test.go b/execx_shadow_test.go
new file mode 100644
index 0000000..c2fffd4
--- /dev/null
+++ b/execx_shadow_test.go
@@ -0,0 +1,143 @@
+package execx
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "regexp"
+ "strings"
+ "sync"
+ "testing"
+)
+
+var stderrMu sync.Mutex
+
+func captureStderr(t *testing.T, fn func()) string {
+ t.Helper()
+
+ stderrMu.Lock()
+ defer stderrMu.Unlock()
+
+ orig := os.Stderr
+ r, w, err := os.Pipe()
+ if err != nil {
+ t.Fatalf("pipe: %v", err)
+ }
+ os.Stderr = w
+
+ fn()
+
+ _ = w.Close()
+ os.Stderr = orig
+
+ var buf bytes.Buffer
+ _, _ = io.Copy(&buf, r)
+ _ = r.Close()
+
+ return buf.String()
+}
+
+func stripANSI(s string) string {
+ re := regexp.MustCompile(`\x1b\[[0-9;]*m`)
+ return re.ReplaceAllString(s, "")
+}
+
+func TestShadowPrintDefault(t *testing.T) {
+ out := captureStderr(t, func() {
+ _, _ = Command("printf", "hi").ShadowPrint().Run()
+ })
+ plain := stripANSI(out)
+ if !strings.Contains(plain, "execx > printf hi") {
+ t.Fatalf("expected shadow print, got %q", plain)
+ }
+ if !strings.Contains(plain, "execx > printf hi (") {
+ t.Fatalf("expected duration line, got %q", plain)
+ }
+}
+
+func TestShadowPrintPrefix(t *testing.T) {
+ out := captureStderr(t, func() {
+ _, _ = Command("printf", "hi").ShadowPrintPrefix("run").Run()
+ })
+ plain := stripANSI(out)
+ if !strings.Contains(plain, "run > printf hi") {
+ t.Fatalf("expected prefix, got %q", plain)
+ }
+}
+
+func TestShadowPrintOff(t *testing.T) {
+ out := captureStderr(t, func() {
+ _, _ = Command("printf", "hi").ShadowPrint().ShadowPrintOff().Run()
+ })
+ if strings.TrimSpace(out) != "" {
+ t.Fatalf("expected no output, got %q", out)
+ }
+}
+
+func TestShadowPrintMask(t *testing.T) {
+ out := captureStderr(t, func() {
+ mask := func(cmd string) string {
+ return strings.ReplaceAll(cmd, "secret", "***")
+ }
+ _, _ = Command("printf", "secret").ShadowPrintMask(mask).Run()
+ })
+ plain := stripANSI(out)
+ if !strings.Contains(plain, "printf ***") {
+ t.Fatalf("expected masked output, got %q", plain)
+ }
+}
+
+func TestShadowPrintFormatter(t *testing.T) {
+ out := captureStderr(t, func() {
+ formatter := func(ev ShadowEvent) string {
+ return "shadow:" + string(ev.Phase) + ":" + ev.RawCommand
+ }
+ _, _ = Command("printf", "hi").ShadowPrintFormatter(formatter).Run()
+ })
+ lines := strings.FieldsFunc(strings.TrimSpace(out), func(r rune) bool {
+ return r == '\n' || r == '\r'
+ })
+ if len(lines) != 2 {
+ t.Fatalf("expected 2 lines, got %d: %q", len(lines), out)
+ }
+ if !strings.HasPrefix(lines[0], "shadow:before:printf hi") {
+ t.Fatalf("unexpected before line: %q", lines[0])
+ }
+ if !strings.HasPrefix(lines[1], "shadow:after:printf hi") {
+ t.Fatalf("unexpected after line: %q", lines[1])
+ }
+}
+
+func TestShadowPrintFormatterEmpty(t *testing.T) {
+ out := captureStderr(t, func() {
+ formatter := func(ev ShadowEvent) string {
+ return ""
+ }
+ _, _ = Command("printf", "hi").ShadowPrintFormatter(formatter).Run()
+ })
+ if strings.TrimSpace(out) != "" {
+ t.Fatalf("expected no output, got %q", out)
+ }
+}
+
+func TestShadowCommandPipeline(t *testing.T) {
+ cmd := Command("printf", "go").Pipe("tr", "a-z", "A-Z")
+ if got := cmd.shadowCommand(); got != "printf go | tr a-z A-Z" {
+ t.Fatalf("unexpected shadow command: %q", got)
+ }
+}
+
+func TestShadowPrintAsync(t *testing.T) {
+ out := captureStderr(t, func() {
+ proc := Command("sleep", "0.01").ShadowPrint().Start()
+ _, _ = proc.Wait()
+ })
+ plain := stripANSI(out)
+ if !strings.Contains(plain, "(async)") {
+ t.Fatalf("expected async marker, got %q", plain)
+ }
+}
+
+func TestShadowPrintLineNil(t *testing.T) {
+ shadowPrintLine(nil, ShadowBefore, 0, false)
+}