From 50d87fbe95d9d3a3c9ef24b6c0665df22ad3a9da Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Fri, 2 Jan 2026 19:04:45 -0600
Subject: [PATCH 1/4] feat: decoder pipe
---
README.md | 201 +++++++++++++++++++++-
decode.go | 304 +++++++++++++++++++++++++++++++++
decode_test.go | 226 ++++++++++++++++++++++++
docs/examplegen/main.go | 3 +
examples/creationflags/main.go | 2 +-
examples/decode/main.go | 37 ++++
examples/decodejson/main.go | 24 +++
examples/decodewith/main.go | 24 +++
examples/decodeyaml/main.go | 24 +++
examples/fromcombined/main.go | 25 +++
examples/fromstderr/main.go | 38 +++++
examples/fromstdout/main.go | 25 +++
examples/hidewindow/main.go | 2 +-
examples/into/main.go | 24 +++
examples/shadowprint/main.go | 4 +-
examples/trim/main.go | 25 +++
go.mod | 2 +
go.sum | 4 +
18 files changed, 984 insertions(+), 10 deletions(-)
create mode 100644 decode.go
create mode 100644 decode_test.go
create mode 100644 examples/decode/main.go
create mode 100644 examples/decodejson/main.go
create mode 100644 examples/decodewith/main.go
create mode 100644 examples/decodeyaml/main.go
create mode 100644 examples/fromcombined/main.go
create mode 100644 examples/fromstderr/main.go
create mode 100644 examples/fromstdout/main.go
create mode 100644 examples/into/main.go
create mode 100644 examples/trim/main.go
create mode 100644 go.sum
diff --git a/README.md b/README.md
index a05f408..8e8b4f0 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -167,6 +167,21 @@ res, err := execx.
// Duration: 10.123456ms
```
+## Error handling model
+
+execx returns two error surfaces:
+
+1) `err` (from `Run`, `Output`, `CombinedOutput`, `Wait`, etc) only reports execution failures:
+ - start failures (binary not found, not executable, OS start error)
+ - context cancellations or timeouts (`WithContext`, `WithTimeout`, `WithDeadline`)
+ - pipeline failures based on `PipeStrict` / `PipeBestEffort`
+
+2) `Result.Err` mirrors `err` for convenience; it is not for exit status.
+
+Exit status is always reported via `Result.ExitCode`, even on non-zero exits. A non-zero exit does not automatically produce `err`.
+
+Use `err` when you want to handle execution failures, and check `Result.ExitCode` (or `Result.OK()` / `Result.IsExitCode`) when you care about command success.
+
## Non-goals and design principles
Design principles:
@@ -194,6 +209,7 @@ All public APIs are covered by runnable examples under `./examples`, and the tes
| **Construction** | [Command](#command) |
| **Context** | [WithContext](#withcontext) [WithDeadline](#withdeadline) [WithTimeout](#withtimeout) |
| **Debugging** | [Args](#args) [ShellEscaped](#shellescaped) [String](#string) |
+| **Decoding** | [Decode](#decode) [DecodeJSON](#decodejson) [DecodeYAML](#decodeyaml) [FromCombined](#fromcombined) [FromStderr](#fromstderr) [FromStdout](#fromstdout) [Into](#into) [DecodeWith](#decodewith) [Trim](#trim) |
| **Environment** | [Env](#env) [EnvAppend](#envappend) [EnvInherit](#envinherit) [EnvList](#envlist) [EnvOnly](#envonly) |
| **Errors** | [Error](#error) [Unwrap](#unwrap) |
| **Execution** | [CombinedOutput](#combinedoutput) [Output](#output) [OutputBytes](#outputbytes) [OutputTrimmed](#outputtrimmed) [Run](#run) [Start](#start) |
@@ -299,6 +315,179 @@ fmt.Println(cmd.String())
// #string echo "hello world" it's
```
+## Decoding
+
+### Decode
+
+Decode configures a custom decoder for this command.
+
+```go
+type payload struct {
+ Name string
+}
+decoder := execx.DecoderFunc(func(data []byte, dst any) error {
+ out, ok := dst.(*payload)
+ if !ok {
+ return fmt.Errorf("expected *payload")
+ }
+ _, val, ok := strings.Cut(string(data), "=")
+ if !ok {
+ return fmt.Errorf("invalid payload")
+ }
+ out.Name = val
+ return nil
+})
+var out payload
+_ = execx.Command("printf", "name=gopher").
+ Decode(decoder).
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### DecodeJSON
+
+DecodeJSON configures JSON decoding for this command.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### DecodeYAML
+
+DecodeYAML configures YAML decoding for this command.
+
+```go
+type payload struct {
+ Name string `yaml:"name"`
+}
+var out payload
+_ = execx.Command("printf", "name: gopher").
+ DecodeYAML().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### FromCombined
+
+FromCombined decodes from combined stdout+stderr.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("sh", "-c", `printf '{"name":"gopher"}'`).
+ DecodeJSON().
+ FromCombined().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### FromStderr
+
+FromStderr decodes from stderr.
+
+```go
+type payload struct {
+ Name string
+}
+decoder := execx.DecoderFunc(func(data []byte, dst any) error {
+ out, ok := dst.(*payload)
+ if !ok {
+ return fmt.Errorf("expected *payload")
+ }
+ _, val, ok := strings.Cut(string(data), "=")
+ if !ok {
+ return fmt.Errorf("invalid payload")
+ }
+ out.Name = val
+ return nil
+})
+var out payload
+_ = execx.Command("sh", "-c", "printf 'name=gopher' 1>&2").
+ Decode(decoder).
+ FromStderr().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### FromStdout
+
+FromStdout decodes from stdout (default).
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ FromStdout().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### Into
+
+Into executes the command and decodes into dst.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### DecodeWith
+
+DecodeWith executes the command and decodes stdout into dst.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeWith(&out, execx.DecoderFunc(json.Unmarshal))
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### Trim
+
+Trim trims whitespace before decoding.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", " {\"name\":\"gopher\"} ").
+ DecodeJSON().
+ Trim().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
## Environment
### Env
@@ -497,7 +686,7 @@ fmt.Println(out)
### CreationFlags
-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.
```go
out, _ := execx.Command("printf", "ok").CreationFlags(execx.CreateNewProcessGroup).Output()
@@ -507,7 +696,7 @@ fmt.Print(out)
### HideWindow
-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.
```go
out, _ := execx.Command("printf", "ok").HideWindow(true).Output()
@@ -517,7 +706,7 @@ fmt.Print(out)
### Pdeathsig
-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.
```go
out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
@@ -527,7 +716,7 @@ fmt.Print(out)
### Setpgid
-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.
```go
out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
@@ -537,7 +726,7 @@ fmt.Print(out)
### Setsid
-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.
```go
out, _ := execx.Command("printf", "ok").Setsid(true).Output()
diff --git a/decode.go b/decode.go
new file mode 100644
index 0000000..7196cc3
--- /dev/null
+++ b/decode.go
@@ -0,0 +1,304 @@
+package execx
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "reflect"
+
+ "gopkg.in/yaml.v3"
+)
+
+// Decoder decodes serialized data into a destination value.
+type Decoder interface {
+ Decode(data []byte, dst any) error
+}
+
+// DecoderFunc adapts a function to a Decoder.
+type DecoderFunc func(data []byte, dst any) error
+
+type decodeSource int
+
+const (
+ decodeStdout decodeSource = iota
+ decodeStderr
+ decodeCombined
+)
+
+type decodeConfig struct {
+ source decodeSource
+ trim bool
+}
+
+func defaultDecodeConfig() decodeConfig {
+ return decodeConfig{source: decodeStdout}
+}
+
+// DecodeChain configures typed decoding for a command.
+type DecodeChain struct {
+ cmd *Cmd
+ decoder Decoder
+ cfg decodeConfig
+}
+
+// Decode configures a custom decoder for this command.
+// @group Decoding
+//
+// Example: decode custom
+//
+// type payload struct {
+// Name string
+// }
+// decoder := execx.DecoderFunc(func(data []byte, dst any) error {
+// out, ok := dst.(*payload)
+// if !ok {
+// return fmt.Errorf("expected *payload")
+// }
+// _, val, ok := strings.Cut(string(data), "=")
+// if !ok {
+// return fmt.Errorf("invalid payload")
+// }
+// out.Name = val
+// return nil
+// })
+// var out payload
+// _ = execx.Command("printf", "name=gopher").
+// Decode(decoder).
+// Into(&out)
+// fmt.Println(out.Name)
+// // #string gopher
+func (c *Cmd) Decode(decoder Decoder) *DecodeChain {
+ return &DecodeChain{
+ cmd: c,
+ decoder: decoder,
+ cfg: defaultDecodeConfig(),
+ }
+}
+
+// Decode implements Decoder.
+func (f DecoderFunc) Decode(data []byte, dst any) error {
+ return f(data, dst)
+}
+
+// DecodeJSON configures JSON decoding for this command.
+// @group Decoding
+//
+// Example: decode json
+//
+// type payload struct {
+// Name string `json:"name"`
+// }
+// var out payload
+// _ = execx.Command("printf", `{"name":"gopher"}`).
+// DecodeJSON().
+// Into(&out)
+// fmt.Println(out.Name)
+// // #string gopher
+func (c *Cmd) DecodeJSON() *DecodeChain {
+ return c.Decode(DecoderFunc(json.Unmarshal))
+}
+
+// DecodeYAML configures YAML decoding for this command.
+// @group Decoding
+//
+// Example: decode yaml
+//
+// type payload struct {
+// Name string `yaml:"name"`
+// }
+// var out payload
+// _ = execx.Command("printf", "name: gopher").
+// DecodeYAML().
+// Into(&out)
+// fmt.Println(out.Name)
+// // #string gopher
+func (c *Cmd) DecodeYAML() *DecodeChain {
+ return c.Decode(DecoderFunc(yaml.Unmarshal))
+}
+
+// FromStdout decodes from stdout (default).
+// @group Decoding
+//
+// Example: decode from stdout
+//
+// type payload struct {
+// Name string `json:"name"`
+// }
+// var out payload
+// _ = execx.Command("printf", `{"name":"gopher"}`).
+// DecodeJSON().
+// FromStdout().
+// Into(&out)
+// fmt.Println(out.Name)
+// // #string gopher
+func (d *DecodeChain) FromStdout() *DecodeChain {
+ d.cfg.source = decodeStdout
+ return d
+}
+
+// FromStderr decodes from stderr.
+// @group Decoding
+//
+// Example: decode from stderr
+//
+// type payload struct {
+// Name string
+// }
+// decoder := execx.DecoderFunc(func(data []byte, dst any) error {
+// out, ok := dst.(*payload)
+// if !ok {
+// return fmt.Errorf("expected *payload")
+// }
+// _, val, ok := strings.Cut(string(data), "=")
+// if !ok {
+// return fmt.Errorf("invalid payload")
+// }
+// out.Name = val
+// return nil
+// })
+// var out payload
+// _ = execx.Command("sh", "-c", "printf 'name=gopher' 1>&2").
+// Decode(decoder).
+// FromStderr().
+// Into(&out)
+// fmt.Println(out.Name)
+// // #string gopher
+func (d *DecodeChain) FromStderr() *DecodeChain {
+ d.cfg.source = decodeStderr
+ return d
+}
+
+// FromCombined decodes from combined stdout+stderr.
+// @group Decoding
+//
+// Example: decode combined
+//
+// type payload struct {
+// Name string `json:"name"`
+// }
+// var out payload
+// _ = execx.Command("sh", "-c", `printf '{"name":"gopher"}'`).
+// DecodeJSON().
+// FromCombined().
+// Into(&out)
+// fmt.Println(out.Name)
+// // #string gopher
+func (d *DecodeChain) FromCombined() *DecodeChain {
+ d.cfg.source = decodeCombined
+ return d
+}
+
+// Trim trims whitespace before decoding.
+// @group Decoding
+//
+// Example: decode trim
+//
+// type payload struct {
+// Name string `json:"name"`
+// }
+// var out payload
+// _ = execx.Command("printf", " {\"name\":\"gopher\"} ").
+// DecodeJSON().
+// Trim().
+// Into(&out)
+// fmt.Println(out.Name)
+// // #string gopher
+func (d *DecodeChain) Trim() *DecodeChain {
+ d.cfg.trim = true
+ return d
+}
+
+// Into executes the command and decodes into dst.
+// @group Decoding
+//
+// Example: decode into
+//
+// type payload struct {
+// Name string `json:"name"`
+// }
+// var out payload
+// _ = execx.Command("printf", `{"name":"gopher"}`).
+// DecodeJSON().
+// Into(&out)
+// fmt.Println(out.Name)
+// // #string gopher
+func (d *DecodeChain) Into(dst any) error {
+ return decodeInto(d.cmd, dst, d.decoder, d.cfg)
+}
+
+// DecodeWith executes the command and decodes stdout into dst.
+// @group Decoding
+//
+// Example: decode with
+//
+// type payload struct {
+// Name string `json:"name"`
+// }
+// var out payload
+// _ = execx.Command("printf", `{"name":"gopher"}`).
+// DecodeWith(&out, execx.DecoderFunc(json.Unmarshal))
+// fmt.Println(out.Name)
+// // #string gopher
+func (c *Cmd) DecodeWith(dst any, decoder Decoder) error {
+ return decodeInto(c, dst, decoder, defaultDecodeConfig())
+}
+
+func decodeInto(c *Cmd, dst any, decoder Decoder, cfg decodeConfig) error {
+ if c == nil {
+ return errors.New("command is nil")
+ }
+ if decoder == nil {
+ return errors.New("decoder is nil")
+ }
+ if dst == nil {
+ return errors.New("destination is nil")
+ }
+ val := reflect.ValueOf(dst)
+ if val.Kind() != reflect.Ptr || val.IsNil() {
+ return errors.New("destination must be a non-nil pointer")
+ }
+ data, err := c.decodeSource(cfg.source)
+ if err != nil {
+ return err
+ }
+ if cfg.trim {
+ data = bytes.TrimSpace(data)
+ }
+ if err := decoder.Decode(data, dst); err != nil {
+ return fmt.Errorf("decode %s: %w", decodeSourceName(cfg.source), err)
+ }
+ return nil
+}
+
+func (c *Cmd) decodeSource(source decodeSource) ([]byte, error) {
+ switch source {
+ case decodeCombined:
+ out, err := c.CombinedOutput()
+ if err != nil {
+ return nil, err
+ }
+ return []byte(out), nil
+ case decodeStderr:
+ res, err := c.Run()
+ if err != nil {
+ return nil, err
+ }
+ return []byte(res.Stderr), nil
+ case decodeStdout:
+ fallthrough
+ default:
+ return c.OutputBytes()
+ }
+}
+
+func decodeSourceName(source decodeSource) string {
+ switch source {
+ case decodeCombined:
+ return "combined output"
+ case decodeStderr:
+ return "stderr"
+ default:
+ return "stdout"
+ }
+}
diff --git a/decode_test.go b/decode_test.go
new file mode 100644
index 0000000..f3c8be3
--- /dev/null
+++ b/decode_test.go
@@ -0,0 +1,226 @@
+package execx
+
+import (
+ "encoding/json"
+ "errors"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+type testPayload struct {
+ Name string `json:"name"`
+}
+
+func TestDecodeYAMLInto(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("decode yaml test uses printf")
+ }
+ type payload struct {
+ Name string `yaml:"name"`
+ }
+ var out payload
+ if err := Command("printf", "name: gopher").
+ DecodeYAML().
+ Into(&out); err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if out.Name != "gopher" {
+ t.Fatalf("unexpected name: %q", out.Name)
+ }
+}
+
+func TestDecodeFromStdout(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("decode stdout test uses printf")
+ }
+ var out testPayload
+ if err := Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ FromStdout().
+ Into(&out); err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if out.Name != "gopher" {
+ t.Fatalf("unexpected name: %q", out.Name)
+ }
+}
+
+func TestDecodeNilCmd(t *testing.T) {
+ var out testPayload
+ err := (*Cmd)(nil).
+ DecodeJSON().
+ Into(&out)
+ if err == nil {
+ t.Fatalf("expected error for nil command")
+ }
+}
+
+func TestDecodeWithJSON(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("output into test uses printf")
+ }
+ var out testPayload
+ err := Command("printf", `{"name":"gopher"}`).
+ DecodeWith(&out, DecoderFunc(json.Unmarshal))
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if out.Name != "gopher" {
+ t.Fatalf("unexpected name: %q", out.Name)
+ }
+}
+
+func TestDecodeTrim(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("decode trim test uses printf")
+ }
+ var out testPayload
+ err := Command("printf", ` {"name":"gopher"} `).
+ DecodeJSON().
+ Trim().
+ Into(&out)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if out.Name != "gopher" {
+ t.Fatalf("unexpected name: %q", out.Name)
+ }
+}
+
+func TestDecodeCombined(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("combined output test uses sh")
+ }
+ var out testPayload
+ err := Command("sh", "-c", `printf '{"name":"gopher"}'`).
+ DecodeJSON().
+ FromCombined().
+ Into(&out)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if out.Name != "gopher" {
+ t.Fatalf("unexpected name: %q", out.Name)
+ }
+}
+
+func TestDecodeCombinedStartError(t *testing.T) {
+ var out testPayload
+ err := Command("execx-does-not-exist").
+ DecodeJSON().
+ FromCombined().
+ Into(&out)
+ if err == nil {
+ t.Fatalf("expected error for combined start failure")
+ }
+}
+
+func TestDecodeCombinedDecodeError(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("combined output test uses sh")
+ }
+ var out testPayload
+ err := Command("sh", "-c", "printf not-json").
+ DecodeJSON().
+ FromCombined().
+ Into(&out)
+ if err == nil || !strings.Contains(err.Error(), "combined output") {
+ t.Fatalf("expected combined output decode error, got %v", err)
+ }
+}
+
+func TestDecodeStderr(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("stderr output test uses sh")
+ }
+ type payload struct {
+ Name string
+ }
+ decoder := DecoderFunc(func(data []byte, dst any) error {
+ out, ok := dst.(*payload)
+ if !ok {
+ return errors.New("expected *payload")
+ }
+ _, val, ok := strings.Cut(string(data), "=")
+ if !ok {
+ return errors.New("invalid payload")
+ }
+ out.Name = val
+ return nil
+ })
+ var out payload
+ err := Command("sh", "-c", "printf 'name=gopher' 1>&2").
+ Decode(decoder).
+ FromStderr().
+ Into(&out)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if out.Name != "gopher" {
+ t.Fatalf("unexpected name: %q", out.Name)
+ }
+}
+
+func TestDecodeStderrStartError(t *testing.T) {
+ var out testPayload
+ err := Command("execx-does-not-exist").
+ DecodeJSON().
+ FromStderr().
+ Into(&out)
+ if err == nil {
+ t.Fatalf("expected error for stderr start failure")
+ }
+}
+
+func TestDecodeStderrDecodeError(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("stderr output test uses sh")
+ }
+ var out testPayload
+ err := Command("sh", "-c", "printf not-json 1>&2").
+ DecodeJSON().
+ FromStderr().
+ Into(&out)
+ if err == nil || !strings.Contains(err.Error(), "stderr") {
+ t.Fatalf("expected stderr decode error, got %v", err)
+ }
+}
+
+func TestDecodeWithErrors(t *testing.T) {
+ err := Command("printf", `{"name":"gopher"}`).
+ DecodeWith(nil, DecoderFunc(json.Unmarshal))
+ if err == nil {
+ t.Fatalf("expected error for nil destination")
+ }
+
+ var out testPayload
+ err = Command("printf", `{"name":"gopher"}`).
+ DecodeWith(out, DecoderFunc(json.Unmarshal))
+ if err == nil {
+ t.Fatalf("expected error for non-pointer destination")
+ }
+
+ err = Command("printf", `{"name":"gopher"}`).
+ DecodeWith(&out, nil)
+ if err == nil {
+ t.Fatalf("expected error for nil decoder")
+ }
+}
+
+func TestDecodeErrorWrap(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("decode error test uses printf")
+ }
+ var out testPayload
+ err := Command("printf", `not-json`).
+ DecodeJSON().
+ Into(&out)
+ if err == nil {
+ t.Fatalf("expected decode error")
+ }
+ var syntaxErr *json.SyntaxError
+ if !errors.As(err, &syntaxErr) && err.Error() == "" {
+ t.Fatalf("expected decode error detail")
+ }
+}
diff --git a/docs/examplegen/main.go b/docs/examplegen/main.go
index 6a9807e..c48734f 100644
--- a/docs/examplegen/main.go
+++ b/docs/examplegen/main.go
@@ -382,6 +382,9 @@ func writeMain(base string, fd *FuncDoc, importPath string) error {
if strings.Contains(ex.Code, "fmt.") {
imports["fmt"] = true
}
+ if strings.Contains(ex.Code, "json.") {
+ imports["encoding/json"] = true
+ }
if strings.Contains(ex.Code, "strings.") {
imports["strings"] = true
}
diff --git a/examples/creationflags/main.go b/examples/creationflags/main.go
index 2556811..3876d16 100644
--- a/examples/creationflags/main.go
+++ b/examples/creationflags/main.go
@@ -9,7 +9,7 @@ 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(execx.CreateNewProcessGroup).Output()
diff --git a/examples/decode/main.go b/examples/decode/main.go
new file mode 100644
index 0000000..d0277e4
--- /dev/null
+++ b/examples/decode/main.go
@@ -0,0 +1,37 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+ "strings"
+)
+
+func main() {
+ // Decode configures a custom decoder for this command.
+
+ // Example: decode custom
+ type payload struct {
+ Name string
+ }
+ decoder := execx.DecoderFunc(func(data []byte, dst any) error {
+ out, ok := dst.(*payload)
+ if !ok {
+ return fmt.Errorf("expected *payload")
+ }
+ _, val, ok := strings.Cut(string(data), "=")
+ if !ok {
+ return fmt.Errorf("invalid payload")
+ }
+ out.Name = val
+ return nil
+ })
+ var out payload
+ _ = execx.Command("printf", "name=gopher").
+ Decode(decoder).
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
diff --git a/examples/decodejson/main.go b/examples/decodejson/main.go
new file mode 100644
index 0000000..8d09a86
--- /dev/null
+++ b/examples/decodejson/main.go
@@ -0,0 +1,24 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // DecodeJSON configures JSON decoding for this command.
+
+ // Example: decode json
+ type payload struct {
+ Name string `json:"name"`
+ }
+ var out payload
+ _ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
diff --git a/examples/decodewith/main.go b/examples/decodewith/main.go
new file mode 100644
index 0000000..849a1c6
--- /dev/null
+++ b/examples/decodewith/main.go
@@ -0,0 +1,24 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // DecodeWith executes the command and decodes stdout into dst.
+
+ // Example: decode with
+ type payload struct {
+ Name string `json:"name"`
+ }
+ var out payload
+ _ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeWith(&out, execx.DecoderFunc(json.Unmarshal))
+ fmt.Println(out.Name)
+ // #string gopher
+}
diff --git a/examples/decodeyaml/main.go b/examples/decodeyaml/main.go
new file mode 100644
index 0000000..762d36a
--- /dev/null
+++ b/examples/decodeyaml/main.go
@@ -0,0 +1,24 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // DecodeYAML configures YAML decoding for this command.
+
+ // Example: decode yaml
+ type payload struct {
+ Name string `yaml:"name"`
+ }
+ var out payload
+ _ = execx.Command("printf", "name: gopher").
+ DecodeYAML().
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
diff --git a/examples/fromcombined/main.go b/examples/fromcombined/main.go
new file mode 100644
index 0000000..e97cfab
--- /dev/null
+++ b/examples/fromcombined/main.go
@@ -0,0 +1,25 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // FromCombined decodes from combined stdout+stderr.
+
+ // Example: decode combined
+ type payload struct {
+ Name string `json:"name"`
+ }
+ var out payload
+ _ = execx.Command("sh", "-c", `printf '{"name":"gopher"}'`).
+ DecodeJSON().
+ FromCombined().
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
diff --git a/examples/fromstderr/main.go b/examples/fromstderr/main.go
new file mode 100644
index 0000000..9e6213d
--- /dev/null
+++ b/examples/fromstderr/main.go
@@ -0,0 +1,38 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+ "strings"
+)
+
+func main() {
+ // FromStderr decodes from stderr.
+
+ // Example: decode from stderr
+ type payload struct {
+ Name string
+ }
+ decoder := execx.DecoderFunc(func(data []byte, dst any) error {
+ out, ok := dst.(*payload)
+ if !ok {
+ return fmt.Errorf("expected *payload")
+ }
+ _, val, ok := strings.Cut(string(data), "=")
+ if !ok {
+ return fmt.Errorf("invalid payload")
+ }
+ out.Name = val
+ return nil
+ })
+ var out payload
+ _ = execx.Command("sh", "-c", "printf 'name=gopher' 1>&2").
+ Decode(decoder).
+ FromStderr().
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
diff --git a/examples/fromstdout/main.go b/examples/fromstdout/main.go
new file mode 100644
index 0000000..3b12e64
--- /dev/null
+++ b/examples/fromstdout/main.go
@@ -0,0 +1,25 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // FromStdout decodes from stdout (default).
+
+ // Example: decode from stdout
+ type payload struct {
+ Name string `json:"name"`
+ }
+ var out payload
+ _ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ FromStdout().
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
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/into/main.go b/examples/into/main.go
new file mode 100644
index 0000000..b1c32db
--- /dev/null
+++ b/examples/into/main.go
@@ -0,0 +1,24 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // Into executes the command and decodes into dst.
+
+ // Example: decode into
+ type payload struct {
+ Name string `json:"name"`
+ }
+ var out payload
+ _ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
diff --git a/examples/shadowprint/main.go b/examples/shadowprint/main.go
index c2a3b66..d16d9ac 100644
--- a/examples/shadowprint/main.go
+++ b/examples/shadowprint/main.go
@@ -13,7 +13,7 @@ func main() {
// ShadowPrint configures shadow printing for this command chain.
// Example: shadow print
- _, _ = execx.Command("echo", "hello world").
+ _, _ = execx.Command("bash", "-c", `echo "hello world"`).
ShadowPrint().
OnStdout(func(line string) { fmt.Println(line) }).
Run()
@@ -28,7 +28,7 @@ func main() {
formatter := func(ev execx.ShadowEvent) string {
return fmt.Sprintf("shadow: %s %s", ev.Phase, ev.Command)
}
- _, _ = execx.Command("echo", "hello world").
+ _, _ = execx.Command("bash", "-c", `echo "hello world"`).
ShadowPrint(
execx.WithPrefix("execx"),
execx.WithMask(mask),
diff --git a/examples/trim/main.go b/examples/trim/main.go
new file mode 100644
index 0000000..baa7f71
--- /dev/null
+++ b/examples/trim/main.go
@@ -0,0 +1,25 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // Trim trims whitespace before decoding.
+
+ // Example: decode trim
+ type payload struct {
+ Name string `json:"name"`
+ }
+ var out payload
+ _ = execx.Command("printf", " {\"name\":\"gopher\"} ").
+ DecodeJSON().
+ Trim().
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
diff --git a/go.mod b/go.mod
index c46bc92..42edfb6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,5 @@
module github.com/goforj/execx
go 1.24.4
+
+require gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..a62c313
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
From 78f25bdf6a6e69bcb928cce26b6e17797527f3c2 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Fri, 2 Jan 2026 19:14:38 -0600
Subject: [PATCH 2/4] docs: clarify
---
README.md | 37 ++++++++++++++++++----------------
decode.go | 3 +++
examples/creationflags/main.go | 2 +-
examples/decodejson/main.go | 1 +
examples/decodeyaml/main.go | 1 +
examples/hidewindow/main.go | 2 +-
examples/pdeathsig/main.go | 2 +-
examples/setpgid/main.go | 2 +-
examples/setsid/main.go | 2 +-
9 files changed, 30 insertions(+), 22 deletions(-)
diff --git a/README.md b/README.md
index 8e8b4f0..fce23b6 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -209,7 +209,7 @@ All public APIs are covered by runnable examples under `./examples`, and the tes
| **Construction** | [Command](#command) |
| **Context** | [WithContext](#withcontext) [WithDeadline](#withdeadline) [WithTimeout](#withtimeout) |
| **Debugging** | [Args](#args) [ShellEscaped](#shellescaped) [String](#string) |
-| **Decoding** | [Decode](#decode) [DecodeJSON](#decodejson) [DecodeYAML](#decodeyaml) [FromCombined](#fromcombined) [FromStderr](#fromstderr) [FromStdout](#fromstdout) [Into](#into) [DecodeWith](#decodewith) [Trim](#trim) |
+| **Decoding** | [Decode](#decode) [DecodeJSON](#decodejson) [DecodeWith](#decodewith) [DecodeYAML](#decodeyaml) [FromCombined](#fromcombined) [FromStderr](#fromstderr) [FromStdout](#fromstdout) [Into](#into) [Trim](#trim) |
| **Environment** | [Env](#env) [EnvAppend](#envappend) [EnvInherit](#envinherit) [EnvList](#envlist) [EnvOnly](#envonly) |
| **Errors** | [Error](#error) [Unwrap](#unwrap) |
| **Execution** | [CombinedOutput](#combinedoutput) [Output](#output) [OutputBytes](#outputbytes) [OutputTrimmed](#outputtrimmed) [Run](#run) [Start](#start) |
@@ -320,6 +320,7 @@ fmt.Println(cmd.String())
### Decode
Decode configures a custom decoder for this command.
+Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
```go
type payload struct {
@@ -348,6 +349,7 @@ fmt.Println(out.Name)
### DecodeJSON
DecodeJSON configures JSON decoding for this command.
+Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
```go
type payload struct {
@@ -361,9 +363,25 @@ fmt.Println(out.Name)
// #string gopher
```
+### DecodeWith
+
+DecodeWith executes the command and decodes stdout into dst.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeWith(&out, execx.DecoderFunc(json.Unmarshal))
+fmt.Println(out.Name)
+// #string gopher
+```
+
### DecodeYAML
DecodeYAML configures YAML decoding for this command.
+Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
```go
type payload struct {
@@ -456,21 +474,6 @@ fmt.Println(out.Name)
// #string gopher
```
-### DecodeWith
-
-DecodeWith executes the command and decodes stdout into dst.
-
-```go
-type payload struct {
- Name string `json:"name"`
-}
-var out payload
-_ = execx.Command("printf", `{"name":"gopher"}`).
- DecodeWith(&out, execx.DecoderFunc(json.Unmarshal))
-fmt.Println(out.Name)
-// #string gopher
-```
-
### Trim
Trim trims whitespace before decoding.
diff --git a/decode.go b/decode.go
index 7196cc3..f381e50 100644
--- a/decode.go
+++ b/decode.go
@@ -43,6 +43,7 @@ type DecodeChain struct {
}
// Decode configures a custom decoder for this command.
+// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
// @group Decoding
//
// Example: decode custom
@@ -82,6 +83,7 @@ func (f DecoderFunc) Decode(data []byte, dst any) error {
}
// DecodeJSON configures JSON decoding for this command.
+// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
// @group Decoding
//
// Example: decode json
@@ -100,6 +102,7 @@ func (c *Cmd) DecodeJSON() *DecodeChain {
}
// DecodeYAML configures YAML decoding for this command.
+// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
// @group Decoding
//
// Example: decode yaml
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/decodejson/main.go b/examples/decodejson/main.go
index 8d09a86..6611ac2 100644
--- a/examples/decodejson/main.go
+++ b/examples/decodejson/main.go
@@ -10,6 +10,7 @@ import (
func main() {
// DecodeJSON configures JSON decoding for this command.
+ // Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
// Example: decode json
type payload struct {
diff --git a/examples/decodeyaml/main.go b/examples/decodeyaml/main.go
index 762d36a..104de3e 100644
--- a/examples/decodeyaml/main.go
+++ b/examples/decodeyaml/main.go
@@ -10,6 +10,7 @@ import (
func main() {
// DecodeYAML configures YAML decoding for this command.
+ // Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
// Example: decode yaml
type payload struct {
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 450a6a6..04a4d12 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -10,7 +10,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()
From 3f998b78cb6e18ad2d652e23fc49fbc1957c8c8f Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Fri, 2 Jan 2026 19:20:22 -0600
Subject: [PATCH 3/4] docs: simplify example
---
README.md | 24 ++++++------------------
decode.go | 18 +++---------------
decode_test.go | 22 +++-------------------
examples/fromstderr/main.go | 19 +++----------------
examples/pdeathsig/main.go | 2 +-
examples/setpgid/main.go | 2 +-
examples/setsid/main.go | 2 +-
7 files changed, 18 insertions(+), 71 deletions(-)
diff --git a/README.md b/README.md
index fce23b6..37d3b50 100644
--- a/README.md
+++ b/README.md
@@ -418,23 +418,11 @@ FromStderr decodes from stderr.
```go
type payload struct {
- Name string
+ Name string `json:"name"`
}
-decoder := execx.DecoderFunc(func(data []byte, dst any) error {
- out, ok := dst.(*payload)
- if !ok {
- return fmt.Errorf("expected *payload")
- }
- _, val, ok := strings.Cut(string(data), "=")
- if !ok {
- return fmt.Errorf("invalid payload")
- }
- out.Name = val
- return nil
-})
var out payload
-_ = execx.Command("sh", "-c", "printf 'name=gopher' 1>&2").
- Decode(decoder).
+_ = execx.Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
+ DecodeJSON().
FromStderr().
Into(&out)
fmt.Println(out.Name)
@@ -709,7 +697,7 @@ fmt.Print(out)
### Pdeathsig
-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.
```go
out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
@@ -719,7 +707,7 @@ fmt.Print(out)
### Setpgid
-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.
```go
out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
@@ -729,7 +717,7 @@ fmt.Print(out)
### Setsid
-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.
```go
out, _ := execx.Command("printf", "ok").Setsid(true).Output()
diff --git a/decode.go b/decode.go
index f381e50..36c9115 100644
--- a/decode.go
+++ b/decode.go
@@ -146,23 +146,11 @@ func (d *DecodeChain) FromStdout() *DecodeChain {
// Example: decode from stderr
//
// type payload struct {
-// Name string
+// Name string `json:"name"`
// }
-// decoder := execx.DecoderFunc(func(data []byte, dst any) error {
-// out, ok := dst.(*payload)
-// if !ok {
-// return fmt.Errorf("expected *payload")
-// }
-// _, val, ok := strings.Cut(string(data), "=")
-// if !ok {
-// return fmt.Errorf("invalid payload")
-// }
-// out.Name = val
-// return nil
-// })
// var out payload
-// _ = execx.Command("sh", "-c", "printf 'name=gopher' 1>&2").
-// Decode(decoder).
+// _ = execx.Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
+// DecodeJSON().
// FromStderr().
// Into(&out)
// fmt.Println(out.Name)
diff --git a/decode_test.go b/decode_test.go
index f3c8be3..513e6da 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"runtime"
- "strings"
"testing"
)
@@ -134,24 +133,9 @@ func TestDecodeStderr(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("stderr output test uses sh")
}
- type payload struct {
- Name string
- }
- decoder := DecoderFunc(func(data []byte, dst any) error {
- out, ok := dst.(*payload)
- if !ok {
- return errors.New("expected *payload")
- }
- _, val, ok := strings.Cut(string(data), "=")
- if !ok {
- return errors.New("invalid payload")
- }
- out.Name = val
- return nil
- })
- var out payload
- err := Command("sh", "-c", "printf 'name=gopher' 1>&2").
- Decode(decoder).
+ var out testPayload
+ err := Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
+ DecodeJSON().
FromStderr().
Into(&out)
if err != nil {
diff --git a/examples/fromstderr/main.go b/examples/fromstderr/main.go
index 9e6213d..8d87be7 100644
--- a/examples/fromstderr/main.go
+++ b/examples/fromstderr/main.go
@@ -6,7 +6,6 @@ package main
import (
"fmt"
"github.com/goforj/execx"
- "strings"
)
func main() {
@@ -14,23 +13,11 @@ func main() {
// Example: decode from stderr
type payload struct {
- Name string
+ Name string `json:"name"`
}
- decoder := execx.DecoderFunc(func(data []byte, dst any) error {
- out, ok := dst.(*payload)
- if !ok {
- return fmt.Errorf("expected *payload")
- }
- _, val, ok := strings.Cut(string(data), "=")
- if !ok {
- return fmt.Errorf("invalid payload")
- }
- out.Name = val
- return nil
- })
var out payload
- _ = execx.Command("sh", "-c", "printf 'name=gopher' 1>&2").
- Decode(decoder).
+ _ = execx.Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
+ DecodeJSON().
FromStderr().
Into(&out)
fmt.Println(out.Name)
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 04a4d12..132df7a 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -10,7 +10,7 @@ import (
)
func main() {
- // Pdeathsig is a no-op on Windows; on Linux it signals the child when the parent exits.
+ // Pdeathsig is a no-op on non-Linux Unix platforms; 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 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()
From cb7efd54c946250757af7a3ad5d54211832a6505 Mon Sep 17 00:00:00 2001
From: Chris Miles
Date: Fri, 2 Jan 2026 19:22:12 -0600
Subject: [PATCH 4/4] fix: ide
---
README.md | 6 +++---
decode_test.go | 1 +
examples/pdeathsig/main.go | 2 +-
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 37d3b50..259debe 100644
--- a/README.md
+++ b/README.md
@@ -697,7 +697,7 @@ fmt.Print(out)
### Pdeathsig
-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.
```go
out, _ := execx.Command("printf", "ok").Pdeathsig(syscall.SIGTERM).Output()
@@ -707,7 +707,7 @@ fmt.Print(out)
### Setpgid
-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.
```go
out, _ := execx.Command("printf", "ok").Setpgid(true).Output()
@@ -717,7 +717,7 @@ fmt.Print(out)
### Setsid
-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.
```go
out, _ := execx.Command("printf", "ok").Setsid(true).Output()
diff --git a/decode_test.go b/decode_test.go
index 513e6da..43f353c 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"runtime"
+ "strings"
"testing"
)
diff --git a/examples/pdeathsig/main.go b/examples/pdeathsig/main.go
index 132df7a..450a6a6 100644
--- a/examples/pdeathsig/main.go
+++ b/examples/pdeathsig/main.go
@@ -10,7 +10,7 @@ import (
)
func main() {
- // Pdeathsig is a no-op on non-Linux Unix platforms; 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()