Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ["1.24.4", "stable"]
go-version: ["1.27.x", "stable"]

steps:
- name: Checkout
Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
<a href="https://pkg.go.dev/github.com/goforj/execx"><img src="https://pkg.go.dev/badge/github.com/goforj/execx.svg" alt="Go Reference"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
<a href="https://github.com/goforj/execx/actions"><img src="https://github.com/goforj/execx/actions/workflows/test.yml/badge.svg" alt="Go Test"></a>
<a href="https://go.dev"><img src="https://img.shields.io/badge/go-1.24.4+-blue?logo=go" alt="Go version"></a>
<a href="https://go.dev"><img src="https://img.shields.io/badge/go-1.27+-blue?logo=go" alt="Go version"></a>
<img src="https://img.shields.io/github/v/tag/goforj/execx?label=version&sort=semver" alt="Latest tag">
<a href="https://codecov.io/gh/goforj/execx" ><img src="https://codecov.io/github/goforj/execx/graph/badge.svg?token=RBB8T6WQ0U"/></a>
<!-- test-count:embed:start -->
<img src="https://img.shields.io/badge/tests-123-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/tests-125-brightgreen" alt="Tests">
<!-- test-count:embed:end -->
</p>

Expand All @@ -34,7 +34,7 @@ The core contract is deliberately narrow:

## Installation

execx requires Go 1.24.4 or newer.
execx requires Go 1.27 or newer. Projects on older Go releases can pin the v1.1 release line.

```bash
go get github.com/goforj/execx
Expand Down Expand Up @@ -249,7 +249,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) |
| **Decoding** | [As](#as) · [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) · [OnExecCmd](#onexeccmd) · [Output](#output) · [OutputBytes](#outputbytes) · [OutputTrimmed](#outputtrimmed) · [Run](#run) · [Start](#start) |
Expand Down Expand Up @@ -360,6 +360,21 @@ fmt.Println(cmd.String())

## Decoding

### <a id="as"></a>As

As executes the command and decodes its selected output into T.

```go
type payload struct {
Name string `json:"name"`
}
out, err := execx.Command("printf", `{"name":"gopher"}`).
DecodeJSON().
As[payload]()
fmt.Println(err == nil, out.Name)
// true gopher
```

### <a id="decode"></a>Decode

Decode configures a custom decoder for this command.
Expand Down
27 changes: 27 additions & 0 deletions benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import "testing"

var benchmarkArgs []string

// benchmarkPayload prevents decoded benchmark results from being optimized away.
var benchmarkPayload testPayload

// BenchmarkCommandConstruction measures fluent argument assembly without subprocess noise.
func BenchmarkCommandConstruction(b *testing.B) {
b.ReportAllocs()
Expand All @@ -22,3 +25,27 @@ func BenchmarkShellEscaped(b *testing.B) {
benchmarkArgs = []string{cmd.ShellEscaped()}
}
}

// BenchmarkDecodeResult compares caller-owned and generic result decoding through the same command path.
func BenchmarkDecodeResult(b *testing.B) {
b.Run("Into", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var out testPayload
if err := Command("printf", `{"name":"gopher"}`).DecodeJSON().Into(&out); err != nil {
b.Fatalf("Into: %v", err)
}
benchmarkPayload = out
}
})
b.Run("As", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
out, err := Command("printf", `{"name":"gopher"}`).DecodeJSON().As[testPayload]()
if err != nil {
b.Fatalf("As: %v", err)
}
benchmarkPayload = out
}
})
}
19 changes: 19 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,25 @@ func (d *DecodeChain) Into(dst any) error {
return decodeInto(d.cmd, dst, d.decoder, d.cfg)
}

// As executes the command and decodes its selected output into T.
// @group Decoding
//
// Example: decode as a value
//
// type payload struct {
// Name string `json:"name"`
// }
// out, err := execx.Command("printf", `{"name":"gopher"}`).
// DecodeJSON().
// As[payload]()
// fmt.Println(err == nil, out.Name)
// // true gopher
func (d *DecodeChain) As[T any]() (T, error) {
var out T
err := decodeInto(d.cmd, &out, d.decoder, d.cfg)
return out, err
}

// DecodeWith executes the command and decodes stdout into dst.
// @group Decoding
//
Expand Down
42 changes: 42 additions & 0 deletions decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,48 @@ type testPayload struct {
Name string `json:"name"`
}

// TestDecodeAs returns a typed result while preserving the configured decoder and source.
func TestDecodeAs(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("decode as test uses printf")
}

as := (*DecodeChain).As[testPayload]
chain := Command("printf", ` {"name":"gopher"} `).
DecodeJSON().
FromStdout().
Trim()
out, err := as(chain)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if out.Name != "gopher" {
t.Fatalf("unexpected name: %q", out.Name)
}

bound := Command("printf", `{"name":"value"}`).DecodeJSON().As[testPayload]
out, err = bound()
if err != nil || out.Name != "value" {
t.Fatalf("method value returned %+v, %v", out, err)
}
}

// TestDecodeAsErrors preserves command, decoder, and decode failures in the result form.
func TestDecodeAsErrors(t *testing.T) {
if _, err := (*Cmd)(nil).DecodeJSON().As[testPayload](); err == nil {
t.Fatal("expected nil command error")
}
if _, err := Command("printf", `{}`).Decode(nil).As[testPayload](); err == nil {
t.Fatal("expected nil decoder error")
}
if runtime.GOOS == "windows" {
t.Skip("decode error test uses printf")
}
if _, err := Command("printf", `not-json`).DecodeJSON().As[testPayload](); err == nil {
t.Fatal("expected decode error")
}
}

// TestDecodeYAMLInto ensures YAML command output can populate a caller-owned destination.
func TestDecodeYAMLInto(t *testing.T) {
if runtime.GOOS == "windows" {
Expand Down
2 changes: 1 addition & 1 deletion docs/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/goforj/execx/docs

go 1.24.4
go 1.27.0
21 changes: 21 additions & 0 deletions examples/as/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"fmt"
"github.com/goforj/execx"
)

// main keeps this documented example executable so API drift fails during compilation.
func main() {
// As executes the command and decodes its selected output into T.

// Example: decode as a value
type payload struct {
Name string `json:"name"`
}
out, err := execx.Command("printf", `{"name":"gopher"}`).
DecodeJSON().
As[payload]()
fmt.Println(err == nil, out.Name)
// true gopher
}
2 changes: 1 addition & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/goforj/execx/examples

go 1.24.4
go 1.27.0

require github.com/goforj/execx v1.1.0

Expand Down
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
module github.com/goforj/execx

go 1.24.4
go 1.27.0

require (
golang.org/x/sys v0.41.0
golang.org/x/term v0.40.0
gopkg.in/yaml.v3 v3.0.1
)

require golang.org/x/sys v0.41.0
Loading