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 @@ Go Report Card

-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 @@ Latest tag - Tests + Tests Go Report Card

@@ -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) +}