Documentation
¶
Overview ¶
Package pinexec runs shell commands with cancellation, output sanitization, and live streaming.
pinexec is a sandboxed "$SHELL -c" runner shaped for AI coding agents:
- Combined stdout+stderr capture.
- Cross-platform cancellation that kills the entire process group (so go run's compiled binary, npm spawn, make recipes, etc. all die when ctx is cancelled — not just the leader).
- Dual output: live raw chunks (ANSI preserved) via an optional callback for UIs, plus a final ANSI-stripped, binary-sanitized output for LLM context.
- Line + byte truncation with tail-keep; full output spills to a temp file when it exceeds the in-memory threshold.
- Color env injection (CLICOLOR/CLICOLOR_FORCE/FORCE_COLOR) when a live callback is provided, so CLIs that gate ANSI on TTY detection still emit colors.
The headline API is Execute. The truncation and ANSI helpers (TruncateHead, TruncateTail, StripAnsi, AppendColorEnv) are exported for callers that want to apply the same shape to output they produce by other means.
pinexec is dependency-free and Go 1.21+.
Index ¶
Examples ¶
Constants ¶
const ( DefaultMaxLines = 2000 DefaultMaxBytes = 50 * 1024 // 50KB GrepMaxLineLength = 500 // Max chars per grep match line )
Default truncation limits.
Variables ¶
This section is empty.
Functions ¶
func AppendColorEnv ¶
AppendColorEnv appends environment variables that force CLI tools to emit ANSI color codes even when stdout is not a TTY. Covers:
- CLICOLOR=1 — BSD/macOS convention to enable color (ls, etc.)
- CLICOLOR_FORCE=3 — BSD/macOS convention to force color even without TTY
- FORCE_COLOR=1 — Node.js/chalk convention (jest, vitest, etc.)
macOS /bin/ls requires CLICOLOR=1 in addition to CLICOLOR_FORCE=3 to emit color codes when stdout is not a TTY.
Existing values are not overwritten so the user can opt out.
func FormatSize ¶
FormatSize formats bytes as human-readable size.
Types ¶
type Options ¶
type Options struct {
// OnChunk, when non-nil, is invoked with each raw output chunk as it
// arrives. ANSI escape sequences are preserved so the chunks are
// suitable for live display. The chunk is the combined stdout+stderr
// stream of the running command; ordering reflects arrival order, not
// stream of origin.
//
// When OnChunk is non-nil, color-forcing environment variables
// (see [AppendColorEnv]) are injected so tools that gate ANSI output
// on TTY detection still emit colors.
//
// OnChunk is called serially from a single goroutine and may block
// briefly without affecting correctness, but a slow callback will
// back-pressure the underlying read loop and may cause the child
// process to block on output. Keep it fast; copy to a channel if
// you need to do real work.
OnChunk func(chunk string)
}
Options configures a single Execute call.
The zero value is valid: no streaming callback, default truncation, no color env injection.
type Result ¶
type Result struct {
// Output is the combined stdout+stderr of the command, with ANSI
// escape sequences stripped, binary bytes replaced with '?', and
// '\r' removed. If the raw output exceeded the truncation limits
// (see [TruncationOptions]) only the tail is retained and
// [Result.Truncated] is true.
Output string
// ExitCode is the process exit code, or -1 if the command was
// cancelled via context, killed, or did not produce an exit
// status for any other reason.
ExitCode int
// Cancelled is true if the call's context was cancelled before
// the command finished.
Cancelled bool
// Truncated is true if [Result.Output] was truncated.
Truncated bool
// FullOutputPath, when non-empty, is the path to a temp file
// containing the (sanitized, ANSI-stripped) full output of the
// command. The file is created lazily once total output exceeds
// [DefaultMaxBytes] and is not removed by pinexec; the caller
// owns its lifecycle.
FullOutputPath string
}
Result holds the outcome of an Execute call.
func Execute ¶
Execute runs command via $SHELL -c (falling back to /bin/sh), capturing combined stdout+stderr.
The command runs in its own process group on Unix so cancelling ctx kills the entire group, not just the shell — this is important for commands like `go run` that spawn a compiled binary the shell does not directly track. On Windows the standard process-tree termination from exec.CommandContext is used.
Output is sanitized (binary bytes replaced, '\r' stripped) and ANSI-stripped before being stored in Result.Output. Live raw output (ANSI preserved) is streamed via Options.OnChunk when set.
If total output exceeds DefaultMaxBytes, the full sanitized output is also streamed to a temp file whose path is returned in Result.FullOutputPath. The in-memory output is kept to roughly 2×[DefaultMaxBytes] via a rolling window, then further trimmed by TruncateTail to DefaultMaxBytes / DefaultMaxLines for the final Result.Output.
Execute returns a non-nil error only if the child process could not be started or its pipes could not be opened. A non-zero exit status is reported via Result.ExitCode, not as an error.
Execute is safe to call concurrently.
Example ¶
Basic usage: capture combined stdout+stderr and the exit code.
package main
import (
"context"
"fmt"
"strings"
"github.com/kfet/pinexec"
)
func main() {
ctx := context.Background()
res, err := pinexec.Execute(ctx, "echo hello; echo world", nil)
if err != nil {
panic(err)
}
fmt.Println("exit:", res.ExitCode)
fmt.Println(strings.TrimSpace(res.Output))
}
Output: exit: 0 hello world
Example (Streaming) ¶
Stream live chunks (ANSI preserved) while the command runs. The final Result.Output is still ANSI-stripped for downstream consumers.
package main
import (
"context"
"fmt"
"strings"
"github.com/kfet/pinexec"
)
func main() {
ctx := context.Background()
var live strings.Builder
res, _ := pinexec.Execute(ctx, "echo hi", &pinexec.Options{
OnChunk: func(chunk string) {
live.WriteString(chunk)
},
})
fmt.Println("live:", strings.TrimSpace(live.String()))
fmt.Println("stored:", strings.TrimSpace(res.Output))
}
Output: live: hi stored: hi
type TruncationOptions ¶
type TruncationOptions struct {
MaxLines int // 0 means use DefaultMaxLines
MaxBytes int // 0 means use DefaultMaxBytes
}
TruncationOptions configures truncation limits.
type TruncationResult ¶
type TruncationResult struct {
Content string // The (possibly truncated) content
Truncated bool // Whether truncation occurred
TruncatedBy string // "lines", "bytes", or "" if not truncated
TotalLines int // Total lines in original content
TotalBytes int // Total bytes in original content
OutputLines int // Lines in truncated output
OutputBytes int // Bytes in truncated output
LastLinePartial bool // Whether the first line (tail) was partially truncated
FirstLineExceedsLimit bool // Whether the first line exceeds the byte limit (head)
MaxLines int // The max lines limit applied
MaxBytes int // The max bytes limit applied
}
TruncationResult describes the outcome of a truncation operation.
func TruncateHead ¶
func TruncateHead(content string, opts TruncationOptions) TruncationResult
TruncateHead truncates content from the head (keeps first N lines/bytes). Suitable for file reads where you want to see the beginning. Never returns partial lines. If the first line exceeds the byte limit, returns empty content with FirstLineExceedsLimit=true.
func TruncateTail ¶
func TruncateTail(content string, opts TruncationOptions) TruncationResult
TruncateTail truncates content from the tail (keeps last N lines/bytes). Suitable for bash output where you want to see the end (errors, final results). May return partial first line if the last line of original content exceeds the byte limit.