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 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 [DefaultMaxBytes] /
// [DefaultMaxLines] 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 two streams are merged in arrival order; their relative ordering reflects when bytes arrived, not which stream produced them.
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. When onChunk is non-nil, it is invoked with each raw output chunk as it arrives (ANSI preserved) for live display, and color-forcing environment variables (see AppendColorEnv) are injected so CLIs that gate ANSI output on TTY detection still emit colors. onChunk is called serially from a single goroutine; a slow callback back-pressures the read loop and may block the child process — keep it fast.
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. 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", 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.