diff --git a/README.md b/README.md
index a05f408..259debe 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) [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) |
@@ -299,6 +315,170 @@ fmt.Println(cmd.String())
// #string echo "hello world" it's
```
+## Decoding
+
+### Decode
+
+Decode configures a custom decoder for this command.
+Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
+
+```go
+type payload struct {
+ Name string
+}
+decoder := execx.DecoderFunc(func(data []byte, dst any) error {
+ out, ok := dst.(*payload)
+ if !ok {
+ return fmt.Errorf("expected *payload")
+ }
+ _, val, ok := strings.Cut(string(data), "=")
+ if !ok {
+ return fmt.Errorf("invalid payload")
+ }
+ out.Name = val
+ return nil
+})
+var out payload
+_ = execx.Command("printf", "name=gopher").
+ Decode(decoder).
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### DecodeJSON
+
+DecodeJSON configures JSON decoding for this command.
+Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### DecodeWith
+
+DecodeWith executes the command and decodes stdout into dst.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeWith(&out, execx.DecoderFunc(json.Unmarshal))
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### DecodeYAML
+
+DecodeYAML configures YAML decoding for this command.
+Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
+
+```go
+type payload struct {
+ Name string `yaml:"name"`
+}
+var out payload
+_ = execx.Command("printf", "name: gopher").
+ DecodeYAML().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### FromCombined
+
+FromCombined decodes from combined stdout+stderr.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("sh", "-c", `printf '{"name":"gopher"}'`).
+ DecodeJSON().
+ FromCombined().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### FromStderr
+
+FromStderr decodes from stderr.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
+ DecodeJSON().
+ FromStderr().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### FromStdout
+
+FromStdout decodes from stdout (default).
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ FromStdout().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### Into
+
+Into executes the command and decodes into dst.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", `{"name":"gopher"}`).
+ DecodeJSON().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
+### Trim
+
+Trim trims whitespace before decoding.
+
+```go
+type payload struct {
+ Name string `json:"name"`
+}
+var out payload
+_ = execx.Command("printf", " {\"name\":\"gopher\"} ").
+ DecodeJSON().
+ Trim().
+ Into(&out)
+fmt.Println(out.Name)
+// #string gopher
+```
+
## Environment
### Env
@@ -497,7 +677,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 +687,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 +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()
@@ -527,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()
@@ -537,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.go b/decode.go
new file mode 100644
index 0000000..36c9115
--- /dev/null
+++ b/decode.go
@@ -0,0 +1,295 @@
+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.
+// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
+// @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.
+// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
+// @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.
+// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
+// @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 `json:"name"`
+// }
+// var out payload
+// _ = execx.Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
+// DecodeJSON().
+// 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..43f353c
--- /dev/null
+++ b/decode_test.go
@@ -0,0 +1,211 @@
+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")
+ }
+ var out testPayload
+ err := Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
+ DecodeJSON().
+ 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/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..6611ac2
--- /dev/null
+++ b/examples/decodejson/main.go
@@ -0,0 +1,25 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+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 {
+ 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..104de3e
--- /dev/null
+++ b/examples/decodeyaml/main.go
@@ -0,0 +1,25 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+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 {
+ 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..8d87be7
--- /dev/null
+++ b/examples/fromstderr/main.go
@@ -0,0 +1,25 @@
+//go:build ignore
+// +build ignore
+
+package main
+
+import (
+ "fmt"
+ "github.com/goforj/execx"
+)
+
+func main() {
+ // FromStderr decodes from stderr.
+
+ // Example: decode from stderr
+ type payload struct {
+ Name string `json:"name"`
+ }
+ var out payload
+ _ = execx.Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
+ DecodeJSON().
+ FromStderr().
+ Into(&out)
+ fmt.Println(out.Name)
+ // #string gopher
+}
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/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=