diff --git a/README.md b/README.md index a05f408..259debe 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Latest tag - Tests + Tests Go Report Card

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