Documentation
¶
Overview ¶
Package command provides command buffers.
A Buffer represents a command's execution and its output. Buffers begin executing on the first Read and complete at io.EOF.
Buffers may implement WriteBuffer to accept input, like stdin. They may also implement LogBuffer to log diagnostics, like stderr. The standard Buffer is an io.Reader over stdout.
Buffers are created by a Machine. lesiw.io/command/sys is a Machine that creates command buffers on the local system.
Other Machines provided by this package:
- lesiw.io/command/mem - in-memory Machine for examples
- lesiw.io/command/ctr - executes commands in containers
- lesiw.io/command/ssh - executes commands over SSH
- lesiw.io/command/sub - prefixes commands with fixed arguments
- lesiw.io/command/mock - mock Machine for testing
Use NewReader and NewWriter to construct Buffers. NewReader is an io.ReadCloser, where Close() stops the command early. NewWriter is an io.WriteCloser, where Close() closes the input stream and waits for the command to finish.
Buffers are usable with standard io.
// Example only: use OS(ctx, m) and Arch(ctx, m). uname, err := io.ReadAll(m.Command(ctx, "uname", "-a"))
They can be piped with io.Copy.
// Example only: use fs.WriteFile(ctx, FS(m)).
io.Copy(
command.NewWriter(ctx, m, "tee", "hello.txt"),
command.NewReader(ctx, m, "echo", "Hello world!"),
)
Copy is a generalization of io.Copy, allowing three or more buffers to be piped together. Commands in the middle of the Copy must be io.ReadWriter. NewStream to returns an io.ReadWriteCloser for piping.
Helpers are available for common buffer operations. Do creates and executes a Buffer, discarding its output to io.Discard. Read creates and executes a Buffer, then returns its output as a string. Trailing whitespace is removed, like command substitution in a shell. Exec creates and executes a Buffer, streaming output to the terminal rather than capturing it. When possible, the underlying command's standard streams are attached directly to the controlling terminal, letting it run interactively.
Environment variables are part of the context.Context. They can be set using WithEnv and inspected using Env.
m := mem.Machine()
ctx := command.WithEnv(context.Background(), map[string]string{
"CGO_ENABLED": "0",
})
command.Exec(ctx, m, "go", "build", ".")
Files ¶
FS provides a lesiw.io/fs.FS that can be accessed using lesiw.io/fs top-level functions.
fsys := command.FS(m)
fs.WriteFile(ctx, fsys, []byte("Hello world!"), "hello.txt")
If the underlying Machine is a FSMachine, FS will return the lesiw.io/fs.FS presented by the Machine. Otherwise, it will return a lesiw.io/fs.FS that uses commands to provide filesystem access.
The default FS probes the remote system to determine which commands to use for filesystem operations. For example, on a Unix-like system, fs.Remove might use rm, whereas on a Windows system, it might use Remove-Item or del.
For composing file operations with io primitives, use fs.OpenBuffer, fs.CreateBuffer, and fs.AppendBuffer. These return lazy-executing io.ReadCloser and io.WriteCloser that defer opening files until first Read or Write.
io.Copy(
fs.CreateBuffer(ctx, fsys, "output.txt"),
fs.OpenBuffer(ctx, fsys, "input.txt"),
)
Shells ¶
A Machine is a broadly applicable concept. A simple function can be adapted into a Machine via MachineFunc.
Shell provides a useful abstraction over a Machine for Machines that run commands and store state in a filesystem: that is to say, a typical computing environment.
A Shell's methods mirror the top level functions of this package and of lesiw.io/fs.
Commands must be explicitly registered on Shells. This encourages users to use commands only when necessary and to rely on portable abstractions when possible. For instance, reading a file via fs.ReadFile instead of registering "cat".
goMachine := command.Shell(sys.Machine(), "go") goMachine.Exec(ctx, "go", "run", ".")
It is encouraged to register all external commands at the Shell's construction. If commands must be registered later, they can be done by registering that command with Sh.Unshell, which returns the underlying Machine.
sh := command.Shell(sys.Machine())
sh = sh.Handle("go", sh.Unshell())
Here is an example of typical Shell usage. Note that external commands are kept to a minimum and portable operations are preferred where possible - for example, using ReadFile over cat.
ctx, sh := context.Background(), command.Shell(sys.Machine(), "go")
if err := sh.Exec(ctx, "go", "mod", "tidy"); err != nil {
log.Fatalf("go mod tidy failed: %v", err)
}
if err := sh.Exec(ctx, "go", "test", "./..."); err != nil {
log.Fatalf("tests failed: %v", err)
}
ver, err := sh.ReadFile(ctx, "VERSION")
if err != nil {
ver = []byte("dev")
}
if err := sh.MkdirAll(ctx, "bin"); err != nil {
log.Fatalf("failed to create bin directory: %v", err)
}
err = sh.Exec(
command.WithEnv(ctx, map[string]string{
"CGO_ENABLED": "0",
}),
"go", "build",
"-ldflags", fmt.Sprintf(
"-X main.version=%s", strings.TrimSpace(string(ver)),
),
"-o", "bin/app", ".",
)
if err != nil {
log.Fatalf("build failed: %v", err)
}
info, err := sh.Stat(ctx, "bin/app")
if err != nil {
log.Fatalf("binary not found: %v", err)
}
fmt.Printf("Built %s (%d bytes)\n", info.Name(), info.Size())
Cookbook ¶
Some common operations in shellcode expressed as Go with command buffers.
Creating an executable file.
sh.WriteFile( fs.WithFileMode(ctx, 0755), "hello.sh", []byte(`#!/bin/sh echo "Hello world!"`), )
Field-splitting (parsing whitespace-separated fields).
f, err := sh.Open("access.log")
if err != nil {
log.Fatal(err)
}
scn := bufio.NewScanner(f)
for scn.Scan() {
fields := strings.Fields(scn.Text())
if len(fields) > 0 {
fmt.Println("IP:", fields[0])
}
}
Copying a file or directory.
dst, err := remoteSh.Create(ctx, "foo") // Or "foo/" for directory.
if err != nil {
log.Fatal(err)
}
defer dst.Close()
src, err := localSh.Open(ctx, "foo") // Or "foo/" for directory.
if err != nil {
log.Fatal(err)
}
defer src.Close()
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
Command substitution (capturing command output).
version, err := command.Read(ctx, sh, "git", "describe", "--tags")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Building version %s\n", version)
Appending to a file.
f, err := sh.Append(ctx, "app.log")
if err != nil {
log.Fatal(err)
}
defer f.Close()
fmt.Fprintln(f, "Log entry")
Creating a temporary file.
f, err := sh.Temp(ctx, "data") // Or "data/" for directory.
if err != nil {
log.Fatal(err)
}
defer sh.RemoveAll(ctx, f.Path())
defer f.Close()
fmt.Fprintf(f, "input data")
if err := sh.Exec(ctx, "process", f.Path()); err != nil {
log.Fatal(err)
}
Checking if a file exists.
if _, err := sh.Stat(ctx, "config.yaml"); err != nil {
log.Fatal("config.yaml not found")
}
Searching a file for a substring.
f, err := sh.Open(ctx, "app.log")
if err != nil {
log.Fatal(err)
}
scn := bufio.NewScanner(f)
for scn.Scan() {
if strings.Contains(scn.Text(), "ERROR") {
fmt.Println(scn.Text())
}
}
Searching a file with a regular expression.
re := regexp.MustCompile(`\bTODO\b`)
f, err := sh.Open(ctx, "main.go")
if err != nil {
log.Fatal(err)
}
scn := bufio.NewScanner(f)
for scn.Scan() {
if re.MatchString(scn.Text()) {
fmt.Println(scn.Text())
}
}
Testing ¶
For tests requiring simple machines, use MachineFunc. For more complex scenarios, use lesiw.io/command/mock.
Responses queue in a lesiw.io/command/mock.Machine. When deciding which response to return, more specific commands take precedent over less specific ones.
m := new(mock.Machine)
m.Return(strings.NewReader("hello\n"), "echo")
m.Return(strings.NewReader(""), "exit")
m.Return(command.Fail(&command.Error{Code: 1}), "exit", "1")
out, err := command.Read(ctx, m, "echo")
if err != nil {
t.Fatal(err)
}
if out != "hello" {
t.Errorf("got %q, want %q", out, "hello")
}
if err := command.Do(ctx, m, "exit"); err == nil {
t.Error("expected error from exit command")
}
Use lesiw.io/command/mock.Calls to retrieve calls when working with a Shelled lesiw.io/command/mock.Machine.
m := new(mock.Machine)
m.Return(strings.NewReader("main\n"), "git", "branch", "--show-current")
m.Return(strings.NewReader(""), "git", "push", "origin", "main")
sh := command.Shell(m, "git")
branch, err := sh.Read(ctx, "git", "branch", "--show-current")
if err != nil {
t.Fatal(err)
}
if branch != "main" {
t.Errorf("got %q, want %q", branch, "main")
}
if err := sh.Exec(ctx, "git", "push", "origin", "main"); err != nil {
t.Fatal(err)
}
calls := mock.Calls(sh, "git")
if len(calls) != 2 {
t.Errorf("got %d git calls, want 2", len(calls))
}
github.com/google/go-cmp/cmp is useful for comparing calls.
m := new(mock.Machine)
m.Return(strings.NewReader("v1.0.0\n"), "git", "describe", "--tags")
m.Return(strings.NewReader(""), "git", "push", "origin", "v1.0.0")
sh := command.Shell(m, "git")
sh.Read(ctx, "git", "describe", "--tags")
sh.Exec(ctx, "git", "push", "origin", "v1.0.0")
got := mock.Calls(sh, "git")
want := []mock.Call{
{Args: []string{"git", "describe", "--tags"}},
{Args: []string{"git", "push", "origin", "v1.0.0"}},
}
if !cmp.Equal(want, got) {
t.Errorf("git calls mismatch (-want +got):\n%s", cmp.Diff(want, got))
}
Tracing ¶
Trace can optionally be set to any io.Writer, including os.Stderr. Commands are traced when buffers are created via Exec, Read, or Do, before any I/O operations begin. lesiw.io/command/sys provides output that mimics set +x.
Example (Trace) ¶
package main
import (
"context"
"fmt"
"io"
"log"
"os"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
command.Trace = os.Stdout // For capture only: consider os.Stderr instead.
defer func() { command.Trace = io.Discard }()
m, ctx := mem.Machine(), context.Background()
ctx = command.WithEnv(ctx, map[string]string{"MY_VAR": "test"})
out, err := command.Read(ctx, m, "echo", "hello")
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
out, err = command.Read(ctx, m, "echo", "world")
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
}
Output: MY_VAR=test echo hello hello MY_VAR=test echo world world
Index ¶
- Variables
- func Arch(ctx context.Context, m Machine) string
- func Attach(buf Buffer) error
- func Copy(dst io.Writer, src io.Reader, mid ...io.ReadWriter) (written int64, err error)
- func Do(ctx context.Context, m Machine, args ...string) error
- func Env(ctx context.Context, m Machine, key string) string
- func Envs(ctx context.Context) map[string]string
- func Exec(ctx context.Context, m Machine, args ...string) error
- func FS(m Machine) fs.FS
- func Log(buf Buffer, w io.Writer)
- func NewReader(ctx context.Context, m Machine, args ...string) io.ReadCloser
- func NewStream(ctx context.Context, m Machine, args ...string) io.ReadWriteCloser
- func NewWriter(ctx context.Context, m Machine, args ...string) io.WriteCloser
- func NotFound(err error) bool
- func OS(ctx context.Context, m Machine) string
- func Read(ctx context.Context, m Machine, args ...string) (string, error)
- func Shutdown(ctx context.Context, m Machine) error
- func String(buf Buffer) string
- func UnsetEnv(ctx context.Context, name string) context.Context
- func WithEnv(ctx context.Context, env map[string]string) context.Context
- func WithoutEnv(ctx context.Context) context.Context
- type ArchMachine
- type AttachBuffer
- type Buffer
- type Error
- type FSMachine
- type LogBuffer
- type Machine
- type MachineFunc
- type OSMachine
- type Sh
- func (sh *Sh) Abs(ctx context.Context, name string) (string, error)
- func (sh *Sh) Append(ctx context.Context, name string) (fs.WritePathCloser, error)
- func (sh *Sh) AppendBuffer(ctx context.Context, name string) io.WriteCloser
- func (sh *Sh) Arch(ctx context.Context) string
- func (sh *Sh) Chmod(ctx context.Context, name string, mode fs.Mode) error
- func (sh *Sh) Chown(ctx context.Context, name string, uid int, gid int) error
- func (sh *Sh) Chtimes(ctx context.Context, name string, atime time.Time, mtime time.Time) error
- func (sh *Sh) Close() error
- func (sh *Sh) Command(ctx context.Context, args ...string) Buffer
- func (sh *Sh) Create(ctx context.Context, name string) (fs.WritePathCloser, error)
- func (sh *Sh) CreateBuffer(ctx context.Context, name string) io.WriteCloser
- func (sh *Sh) Do(ctx context.Context, args ...string) error
- func (sh *Sh) Env(ctx context.Context, key string) string
- func (sh *Sh) Exec(ctx context.Context, args ...string) error
- func (sh *Sh) FS() fs.FS
- func (sh *Sh) Glob(ctx context.Context, pattern string) ([]string, error)
- func (sh *Sh) Handle(command string, machine Machine) *Sh
- func (sh *Sh) HandleFunc(command string, fn func(context.Context, ...string) Buffer) *Sh
- func (sh *Sh) Localize(ctx context.Context, path string) (string, error)
- func (sh *Sh) Lstat(ctx context.Context, name string) (fs.FileInfo, error)
- func (sh *Sh) Mkdir(ctx context.Context, name string) error
- func (sh *Sh) MkdirAll(ctx context.Context, name string) error
- func (sh *Sh) NewReader(ctx context.Context, args ...string) io.ReadCloser
- func (sh *Sh) NewStream(ctx context.Context, args ...string) io.ReadWriteCloser
- func (sh *Sh) NewWriter(ctx context.Context, args ...string) io.WriteCloser
- func (sh *Sh) OS(ctx context.Context) string
- func (sh *Sh) Open(ctx context.Context, name string) (fs.ReadPathCloser, error)
- func (sh *Sh) OpenBuffer(ctx context.Context, name string) io.ReadCloser
- func (sh *Sh) Read(ctx context.Context, args ...string) (string, error)
- func (sh *Sh) ReadDir(ctx context.Context, name string) iter.Seq2[fs.DirEntry, error]
- func (sh *Sh) ReadFile(ctx context.Context, name string) ([]byte, error)
- func (sh *Sh) ReadLink(ctx context.Context, name string) (string, error)
- func (sh *Sh) Remove(ctx context.Context, name string) error
- func (sh *Sh) RemoveAll(ctx context.Context, name string) error
- func (sh *Sh) Rename(ctx context.Context, oldname string, newname string) error
- func (sh *Sh) Shutdown(ctx context.Context) error
- func (sh *Sh) Stat(ctx context.Context, name string) (fs.FileInfo, error)
- func (sh *Sh) Symlink(ctx context.Context, oldname string, newname string) error
- func (sh *Sh) Temp(ctx context.Context, name string) (fs.WritePathCloser, error)
- func (sh *Sh) Truncate(ctx context.Context, name string, size int64) error
- func (sh *Sh) Unshell() Machine
- func (sh *Sh) Walk(ctx context.Context, root string, depth int) iter.Seq2[fs.DirEntry, error]
- func (sh *Sh) WriteFile(ctx context.Context, name string, data []byte) error
- type ShutdownMachine
- type Unsheller
- type WriteBuffer
Examples ¶
- Package (Trace)
- Copy
- Handle
- HandleFunc
- Read
- Sh.Append
- Sh.Arch
- Sh.Command
- Sh.Create
- Sh.CreateBuffer
- Sh.Env
- Sh.FS
- Sh.FS (ReadDir)
- Sh.FS (Workflow)
- Sh.Glob
- Sh.Mkdir
- Sh.MkdirAll
- Sh.OS
- Sh.Open
- Sh.Read
- Sh.ReadDir
- Sh.ReadFile
- Sh.Remove
- Sh.RemoveAll
- Sh.Rename
- Sh.Stat
- Sh.Unshell
- Sh.Walk
- Sh.WriteFile
- Shell
- WithEnv
- WithEnv (Multiple)
Constants ¶
This section is empty.
Variables ¶
var ( Trace = io.Discard ShTrace = prefix.NewWriter("+ ", stderr) )
var ErrClosed = errors.New("command: write to closed buffer")
ErrClosed is returned when attempting to read from or write to a closed reader or writer.
var ErrReadOnly = errors.New("command: write to read-only buffer")
ErrReadOnly is returned when attempting to write to a read-only command.
Functions ¶
func Arch ¶
Arch detects the architecture of the given machine by probing it with various commands. It returns normalized GOARCH values: "amd64", "arm64", "386", "arm", or "unknown".
If m implements ArchMachine, Arch() calls m.Arch(ctx) and uses the returned value. If m.Arch(ctx) returns an empty string, Arch falls back to probing with commands.
Arch automatically pierces through Shell layers by trying each probe command first on the given machine, then unshelling and retrying if the command is not found. This allows Shell handlers to override probe commands for testing while still falling back to the underlying system.
func Attach ¶
Attach attaches buf to the controlling terminal if it implements AttachBuffer. Does nothing if buf does not implement AttachBuffer.
func Copy ¶
Copy copies the output of each stream into the input of the next stream.
Copy uses io.Copy internally, which automatically optimizes for io.ReaderFrom and io.WriterTo implementations. When using NewWriter(), its io.ReaderFrom implementation will automatically close stdin after copying.
The mid stages must be both readable and writable (io.ReadWriter). Use NewStream() to wrap Buffer instances for use in pipelines.
Example ¶
package main
import (
"bytes"
"context"
"fmt"
"log"
"strings"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, m := context.Background(), mem.Machine()
var buf bytes.Buffer
_, err := command.Copy(
&buf,
strings.NewReader("hello world"),
command.NewStream(ctx, m, "tr", "a-z", "A-Z"),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
}
Output: HELLO WORLD
func Do ¶
Do executes a command for its side effects, discarding output. Only the error status is returned.
If the command fails, the error will contain exit code and log output.
func Env ¶
Env returns the value of the environment variable named by key. It first checks the context, then falls back to querying the machine. Returns an empty string if the variable is unset.
func Exec ¶
Exec executes a command and waits for it to complete. The command's output is attached to the controlling terminal.
Unlike Read, errors returned by Exec will not include log output.
func FS ¶
FS returns a filesystem (lesiw.io/fs.FS) that executes commands on m to perform filesystem operations.
If m implements FSMachine, FS() calls m.FS(ctx) and uses the returned filesystem. If m.FS(ctx) returns nil, FS falls back to creating a command-based filesystem.
func Log ¶
Log sets the log destination for buf if it implements LogBuffer. Does nothing if buf does not implement LogBuffer.
func NewReader ¶
NewReader creates a read-only command that cancels on Close.
The command starts lazily on the first Read() call. Close() cancels the underlying context to immediately terminate the command, which is appropriate for read-only operations where the user has signaled they're done reading.
If Close() is called before any Read(), the command never starts.
func NewStream ¶
NewStream creates a bidirectional command stream with full Read/Write/Close access.
The returned io.ReadWriteCloser provides direct access to the command's stdin (Write), stdout (Read), and stdin close signal (Close).
If the underlying command does not support writing (is read-only), Write() will return an error. Close() closes stdin if supported, otherwise it is a no-op.
NewStream is primarily useful with command.Copy for pipeline composition. For most use cases, prefer NewReader (read-only with cancellation) or NewWriter (write-only with completion wait).
func NewWriter ¶
NewWriter creates a write-only command that waits for completion on Close.
The command starts lazily on the first Write() call. Close() waits for the command to complete gracefully by closing stdin and reading any output, which is appropriate for write-only operations that must finish processing before the operation is considered complete.
If Close() is called before any Write(), the command never starts.
NewWriter implements io.ReaderFrom for optimized copying. When io.Copy detects this, it will auto-close stdin after the source reaches EOF.
func NotFound ¶
NotFound returns true if err represents a command that failed to start, typically indicating the command was not found.
A command.Error is considered "not found" when Err is non-nil and Code is 0. This combination means the command never ran (failed to start).
NotFound uses errors.As to probe the error chain for a command.Error. If no command.Error exists in the chain, NotFound returns false.
Example:
_, err := command.Read(ctx, sh, "nonexistent")
if command.NotFound(err) {
// Command wasn't found or failed to start
}
func OS ¶
OS detects the operating system of the given machine by probing it with various commands. It returns normalized GOOS values: "linux", "darwin", "freebsd", "openbsd", "netbsd", "dragonfly", "windows", or "unknown".
If m implements OSMachine, OS() calls m.OS(ctx) and uses the returned value. If m.OS(ctx) returns an empty string, OS falls back to probing with commands.
OS automatically pierces through Shell layers by trying each probe command first on the given machine, then unshelling and retrying if the command is not found. This allows Shell handlers to override probe commands for testing while still falling back to the underlying system.
func Read ¶
Read executes a command and returns its output as a string. All trailing whitespace is stripped from the output. For exact output, use io.ReadAll.
If the command fails, the error will contain an exit code and log output.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, m := context.Background(), mem.Machine()
out, err := command.Read(ctx, m, "echo", "hello world")
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
}
Output: hello world
func Shutdown ¶
Shutdown shuts down the machine if it implements ShutdownMachine. Returns nil if the machine does not implement ShutdownMachine.
The context passed to Shutdown is derived using context.WithoutCancel to ensure cleanup can complete even after the parent context is canceled.
func String ¶
String returns the string representation of buf. If buf implements fmt.Stringer, returns the result of String(). Otherwise, returns the type in angle brackets (e.g., "<*pkg.Type>").
func WithEnv ¶
WithEnv returns a new context with the provided environment variables merged with any existing environment variables in ctx.
Example ¶
package main
import (
"context"
"fmt"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, m := context.Background(), mem.Machine()
ctx = command.WithEnv(ctx, map[string]string{
"HOME": "/home/mem",
})
fmt.Println("HOME:", command.Env(ctx, m, "HOME"))
}
Output: HOME: /home/mem
Example (Multiple) ¶
package main
import (
"context"
"fmt"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
m := mem.Machine()
ctx1 := command.WithEnv(context.Background(), map[string]string{
"HOME": "/",
"TEST": "foobar",
})
ctx2 := command.WithEnv(ctx1, map[string]string{
"HOME": "/home/example",
})
fmt.Println("ctx1(HOME):", command.Env(ctx1, m, "HOME"))
fmt.Println("ctx1(TEST):", command.Env(ctx1, m, "TEST"))
fmt.Println("ctx2(HOME):", command.Env(ctx2, m, "HOME"))
fmt.Println("ctx2(TEST):", command.Env(ctx2, m, "TEST"))
}
Output: ctx1(HOME): / ctx1(TEST): foobar ctx2(HOME): /home/example ctx2(TEST): foobar
func WithoutEnv ¶
WithoutEnv returns a new context with all environment variables removed. This is similar to context.WithoutCancel - it preserves all other values in the context (working directory, deadlines, etc.) while clearing only the environment variables.
This is useful when environment variables have been converted to another form (e.g., command-line arguments for SSH) and should not be passed through to the underlying Machine.
Types ¶
type ArchMachine ¶
type ArchMachine interface {
Machine
// Arch returns the architecture type of this Machine.
Arch(ctx context.Context) string
}
ArchMachine is an optional interface that allows a Machine to provide its own architecture detection implementation.
When Arch() returns a non-empty string, the command.Arch() function will use this value instead of probing the machine with commands.
type AttachBuffer ¶
type AttachBuffer interface {
Buffer
// Attach connects the command to the controlling terminal.
// Both stdin and stdout must be connected to allow interactive use.
// The command should start immediately if not already started.
//
// After Attach returns, the buffer must remain readable exactly once.
// The single Read call must block until the command completes,
// then return 0 bytes read and io.EOF.
//
// Implementations must handle terminal control sequences, raw mode,
// and proper cleanup of terminal state.
Attach() error
}
AttachBuffer is an optional interface for terminal-attached buffers. Terminal-attached buffers connect the command directly to the terminal for interactive use.
After calling Attach, the buffer must still be readable exactly once to observe command completion, but the Read must return 0 bytes and EOF.
type Buffer ¶
type Buffer interface {
// Read reads output from the command.
// The command starts on first Read and completes at EOF.
// Implementations must return EOF when the command terminates.
// Multiple reads may be required to consume all output.
io.Reader
}
Buffer represents a command's execution. Buffers provide read access to command output. Reading drives execution and returns output until the command completes.
Buffers may implement additional interfaces for extended capabilities:
- AttachBuffer - connect to controlling terminal
- LogBuffer - capture diagnostic output
- WriteBuffer - provide input to the command
type Error ¶
type Error struct {
// Log contains the log output. This usually corresponds to stderr.
Log []byte
// Err is the underlying error.
Err error
// Code is the exit code. A value of 0 does not indicate success.
Code int
}
Error represents a command execution failure.
Commands attached to their controlling terminal via Exec will have an empty Log, since stderr is attached directly to the terminal rather than being captured.
type FSMachine ¶
FSMachine is an optional interface that allows a Machine to provide its own filesystem implementation.
When FS() returns a non-nil fs.FS, the command.FS() function will use this filesystem instead of creating a command-based one.
type LogBuffer ¶
type LogBuffer interface {
Buffer
// Log sets the destination for diagnostic output (stderr).
// Implementations must write all stderr output to w.
// This is typically called before any Read to ensure stderr is captured.
// Multiple Log calls should replace the previous destination.
Log(io.Writer)
}
LogBuffer is an optional interface for buffers with diagnostic output. Buffers with diagnostic output can capture stderr separately from stdout.
type Machine ¶
type Machine interface {
// Command instantiates a command with the given context and arguments.
// Environment variables are extracted from ctx using Envs.
// The returned Buffer represents the command's execution.
// Reading to EOF drives command execution to completion.
Command(ctx context.Context, arg ...string) Buffer
}
A Machine executes commands.
Machines may implement additional interfaces for extended capabilities:
- ArchMachine - architecture detection
- FSMachine - filesystem access
- OSMachine - OS detection
- ShutdownMachine - graceful shutdown
func Handle ¶
Handle registers a handler for a specific command name on the given machine. If m is already a Sh, the handler is added to it. Otherwise, a new Sh is created with m as the core machine, with fallback enabled to preserve visibility of the underlying machine's commands.
This allows users to work with Machine as their primary interface while building up a shell:
m := sys.Machine() m = command.Handle(m, "jq", jqMachine) m = command.Handle(m, "go", goMachine) // m is now routing with fallback to sys.Machine() commands
The handled command takes precedence, but unhandled commands still work:
m := sys.Machine() m = command.Handle(m, "echo", customEchoMachine) m.Command(ctx, "echo", "hello") // Uses customEchoMachine m.Command(ctx, "cat", "file") // Falls back to sys.Machine()
For more ergonomic usage, consider using Sh methods directly:
sh := command.Shell(sys.Machine())
sh.Handle("jq", jqMachine).Handle("go", goMachine)
Example ¶
package main
import (
"context"
"fmt"
"log"
"strings"
"lesiw.io/command"
"lesiw.io/command/mem"
"lesiw.io/command/mock"
)
func main() {
m, ctx := mem.Machine(), context.Background()
uname := new(mock.Machine)
uname.Return(strings.NewReader("fakeOS"), "uname")
m = command.Handle(m, "uname", uname)
str, err := command.Read(ctx, m, "uname")
if err != nil {
log.Fatal(err)
}
fmt.Println(str)
}
Output: fakeOS
func HandleFunc ¶
HandleFunc is a convenience wrapper around Handle for function handlers. This matches the pattern of http.HandleFunc.
m := sys.Machine()
m = command.HandleFunc(m, "echo",
func(ctx context.Context, args ...string) Buffer {
// Custom echo implementation
return customMachine.Command(ctx, args...)
})
For more ergonomic usage, consider using Sh methods directly:
sh := command.Shell(sys.Machine())
sh.HandleFunc("echo",
func(ctx context.Context, args ...string) Buffer {
return customMachine.Command(ctx, args...)
})
Example ¶
package main
import (
"context"
"fmt"
"log"
"strings"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
m, ctx := mem.Machine(), context.Background()
m = command.HandleFunc(m, "uppercase",
func(ctx context.Context, args ...string) command.Buffer {
return strings.NewReader(strings.ToUpper(args[1]))
},
)
out, err := command.Read(ctx, m, "uppercase", "hello")
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
}
Output: HELLO
func Unshell ¶
Unshell returns a version of the machine with fallback routing to inner layers. This is the opposite of Shell - it removes explicit routing protection and allows commands to fall back to underlying implementations.
If m implements the Unsheller interface and Unshell() returns non-nil, that machine is returned. Otherwise, m itself is returned unchanged.
IMPORTANT: Unshell breaks portability guarantees. While Shell ensures code only uses explicitly registered commands, Unshell removes this protection and allows fallback to underlying machine implementations. Code using Unshell may inadvertently depend on commands available in development but not in production.
Use Unshell only for specific purposes like probing (OS, Arch, FS detection) where you intentionally want to query the underlying system. For regular command execution, use Shell's explicit routing.
Example:
sh := command.Shell(sys.Machine())
sh.Handle("jq", jqMachine)
// Regular use - only "jq" works, others return "command not found"
sh.Read(ctx, "cat", "file") // Error: command not found
// Unshell for probing - falls back to sys.Machine
m := command.Unshell(sh)
command.OS(ctx, m) // Works! Falls back to sys.Machine's uname
type MachineFunc ¶
MachineFunc is an adapter to allow ordinary functions to be used as Machines. This is similar to http.HandlerFunc.
type OSMachine ¶
type OSMachine interface {
Machine
// OS returns the operating system this Machine is running.
OS(ctx context.Context) string
}
OSMachine is an optional interface that allows a Machine to provide its own OS detection implementation.
When OS() returns a non-empty string, the command.OS() function will use this value instead of probing the machine with commands.
type Sh ¶
type Sh struct {
// contains filtered or unexported fields
}
Sh is a Machine that routes commands to different machines based on command name, similar to how a system shell routes commands via $PATH.
Unlike a raw Machine, a Sh requires explicit registration of all commands. Unregistered commands return "command not found" errors. This creates a controlled environment with explicit command provenance.
A Sh wraps a "core" machine (accessible via the Unwrapper interface) which is used for operations like OS detection and filesystem probing, but not for direct command execution unless explicitly routed.
Sh provides convenient methods for common operations that delegate to package-level functions. This provides an ergonomic API while maintaining flexibility for code that needs to work with the Machine interface.
Example:
sh := command.Shell(sys.Machine())
sh = sh.Handle("jq", jqMachine)
sh = sh.Handle("go", goMachine)
// Ergonomic method calls
out, err := sh.Read(ctx, "jq", ".foo") // ✓
data, err := sh.ReadFile(ctx, "config.yaml")
// Unregistered commands fail
sh.Read(ctx, "cat", "file") // ✗ command not found
func Shell ¶
Shell creates a new shell that wraps the given core machine. Commands must be explicitly registered via Handle to be accessible. The core machine is accessible via the Unwrapper interface for operations like OS detection and filesystem probing.
Optional commands can be provided as varargs. Each specified command will be routed to the underlying machine via sh.Handle(cmd, sh.Unshell()). This is equivalent to manually calling:
sh := command.Shell(core)
for _, cmd := range commands {
sh = sh.Handle(cmd, sh.Unshell())
}
Example:
// Whitelist specific commands sh := command.Shell(sys.Machine(), "go", "git", "make") sh.Read(ctx, "go", "version") // ✓ Works sh.Read(ctx, "cat", "file") // ✗ command not found
This follows the exec.Command naming pattern where the constructor has the longer name and returns a pointer to the shorter type name.
Example ¶
package main
import (
"bytes"
"context"
"fmt"
"log"
"strings"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx := context.Background()
sh := command.Shell(mem.Machine(), "tr", "cat")
var buf bytes.Buffer
_, err := command.Copy(
&buf,
strings.NewReader("hello"),
command.NewStream(ctx, sh, "tr", "a-z", "A-Z"),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
}
Output: HELLO
func (*Sh) Abs ¶
Abs returns an absolute representation of path within the filesystem.
If the path is already absolute, Abs returns it cleaned. If the path is relative, Abs attempts to resolve it to an absolute path.
The returned path format depends on the filesystem implementation. Local filesystems return OS-specific absolute paths (e.g., /home/user/file or C:\Users\file). Remote filesystems may return URLs (e.g., s3://bucket/key or https://server/path).
Files ¶
Returns an absolute representation of the file path.
Requires: lesiw.io/fs.AbsFS || (absolute lesiw.io/fs.WorkDir in ctx)
Directories ¶
Returns an absolute representation of the directory path.
Requires: lesiw.io/fs.AbsFS || (absolute lesiw.io/fs.WorkDir in ctx)
Similar capabilities: path/filepath.Abs, realpath, pwd.
This is a convenience method that calls lesiw.io/fs.Abs.
func (*Sh) Append ¶
Append opens a file for appending or adds files to a directory. Analogous to: os.OpenFile with O_APPEND, echo >>, tar (append mode), 9P Topen with OAPPEND.
If the parent directory does not exist and the filesystem implements lesiw.io/fs.MkdirFS, Append automatically creates the parent directories with mode 0755 (or the mode specified via lesiw.io/fs.WithDirMode).
The returned lesiw.io/fs.WritePathCloser must be closed when done. The Path() method returns the native filesystem path, or the input path if localization is not supported.
Files ¶
Writes are added to the end of the file. If the file does not exist, it is created with mode 0644 (or the mode specified via lesiw.io/fs.WithFileMode).
Requires: lesiw.io/fs.AppendFS || (lesiw.io/fs.FS && lesiw.io/fs.CreateFS)
Directories ¶
A trailing slash returns a tar stream writer that extracts files into the directory. The directory is created if it doesn't exist. Existing files with the same names are overwritten, but other files in the directory are preserved.
Requires: lesiw.io/fs.AppendDirFS || lesiw.io/fs.CreateFS
This is a convenience method that calls lesiw.io/fs.Append.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "log.txt", []byte("line1\n")); err != nil {
log.Fatal(err)
}
f, err := sh.Append(ctx, "log.txt")
if err != nil {
log.Fatal(err)
}
if _, err := f.Write([]byte("line2\n")); err != nil {
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
content, err := sh.ReadFile(ctx, "log.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
}
Output: line1 line2
func (*Sh) AppendBuffer ¶
AppendBuffer returns a lazy-executing writer for appending to the file at name. The file is opened for appending on first Write(), not when AppendBuffer is called. Errors from opening the file are returned from Write(), not AppendBuffer().
Use AppendBuffer when you only need to append bytes and don't need File metadata (Path, Stat, etc.). For metadata access, use Append instead.
Example:
io.Copy(fs.AppendBuffer(ctx, fsys, "log.txt"), src)
This is a convenience method that calls lesiw.io/fs.AppendBuffer.
func (*Sh) Arch ¶
Arch returns the architecture for this shell. The value is detected only once on first use and cached for performance. Returns normalized GOARCH values: "amd64", "arm64", "386", "arm", or "unknown".
Example ¶
package main
import (
"context"
"fmt"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
// Architecture detection is cached after first call
arch := sh.Arch(ctx)
fmt.Println(arch)
}
Output: amd64
func (*Sh) Chmod ¶
Chmod changes the mode of the named file to mode. Analogous to: os.Chmod, chmod, 9P Twstat.
Requires: lesiw.io/fs.ChmodFS
This is a convenience method that calls lesiw.io/fs.Chmod.
func (*Sh) Chown ¶
Chown changes the numeric uid and gid of the named file. Analogous to: os.Chown, os.Lchown, chown, 9P Twstat. This is typically a Unix-specific operation.
Requires: lesiw.io/fs.ChownFS
This is a convenience method that calls lesiw.io/fs.Chown.
func (*Sh) Chtimes ¶
Chtimes changes the access and modification times of the named file. A zero time.Time value will leave the corresponding file time unchanged. Analogous to: os.Chtimes, touch -t, 9P Twstat.
Requires: lesiw.io/fs.ChtimesFS
This is a convenience method that calls lesiw.io/fs.Chtimes.
func (*Sh) Close ¶
Close closes a filesystem if it implements io.Closer.
This is a convenience method that calls lesiw.io/fs.Close.
func (*Sh) Command ¶
Command implements the Machine interface. It routes the command to the registered machine based on the command name (args[0]). If no machine is registered, behavior depends on the fallback flag: with fallback, falls back to inner machine; without fallback, returns error.
Example ¶
package main
import (
"context"
"fmt"
"log"
"strings"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine(), "tr")
var buf strings.Builder
_, err := command.Copy(
&buf,
strings.NewReader("hello world"),
sh.NewStream(ctx, "tr", "a-z", "A-Z"),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
}
Output: HELLO WORLD
func (*Sh) Create ¶
Create creates or truncates the named file for writing. Analogous to: os.Create, touch, echo >, tar, 9P Tcreate, S3 PutObject.
If the parent directory does not exist and the filesystem implements lesiw.io/fs.MkdirFS, Create automatically creates the parent directories with mode 0755 (or the mode specified via lesiw.io/fs.WithDirMode).
The returned lesiw.io/fs.WritePathCloser must be closed when done. The Path() method returns the native filesystem path, or the input path if localization is not supported.
Files ¶
If the file already exists, it is truncated. If the file does not exist, it is created with mode 0644 (or the mode specified via lesiw.io/fs.WithFileMode).
Requires: lesiw.io/fs.CreateFS
Directories ¶
A trailing slash empties the directory (or creates it if it doesn't exist) and returns a tar stream writer for extracting files into it. This is equivalent to Truncate(name, 0) followed by Append(name).
Requires: See lesiw.io/fs.Truncate and lesiw.io/fs.Append requirements
This is a convenience method that calls lesiw.io/fs.Create.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
f, err := sh.Create(ctx, "new.txt")
if err != nil {
log.Fatal(err)
}
if _, err := f.Write([]byte("created")); err != nil {
log.Fatal(err)
}
if err := f.Close(); err != nil {
log.Fatal(err)
}
content, err := sh.ReadFile(ctx, "new.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
}
Output: created
func (*Sh) CreateBuffer ¶
CreateBuffer returns a lazy-executing writer for the file at name. The file is created on first Write(), not when CreateBuffer is called. Errors from creating the file are returned from Write(), not CreateBuffer().
Use CreateBuffer when you only need to write bytes and don't need File metadata (Path, Stat, etc.). For metadata access, use Create instead.
Example:
io.Copy(fs.CreateBuffer(ctx, fsys, "output.txt"), src)
This is a convenience method that calls lesiw.io/fs.CreateBuffer.
Example ¶
package main
import (
"context"
"fmt"
"io"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx := context.Background()
sh := command.Shell(mem.Machine(), "echo")
_, err := io.Copy(
sh.CreateBuffer(ctx, "output.txt"),
sh.NewReader(ctx, "echo", "Hello, World!"),
)
if err != nil {
log.Fatal(err)
}
content, err := sh.ReadFile(ctx, "output.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
}
Output: Hello, World!
func (*Sh) Do ¶
Do executes a command for its side effects, discarding output. Only the error status is returned.
If the command fails, the error will contain exit code and log output.
This is a convenience method that calls Do.
func (*Sh) Env ¶
Env returns the value of the environment variable named by key. It probes the inner machine (sh.m) to retrieve the value, piercing through Shell layers just like OS() and Arch() do.
This is a convenience method that calls Env.
Example ¶
package main
import (
"context"
"fmt"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx := context.Background()
ctx = command.WithEnv(ctx, map[string]string{
"MY_VAR": "test_value",
})
sh := command.Shell(mem.Machine())
val := sh.Env(ctx, "MY_VAR")
fmt.Println(val)
}
Output: test_value
func (*Sh) Exec ¶
Exec executes a command and waits for it to complete. The command's output is attached to the controlling terminal.
Unlike Read, errors returned by Exec will not include log output.
This is a convenience method that calls Exec.
func (*Sh) FS ¶
FS returns the filesystem for this shell. The filesystem is created only once on first use and cached for performance.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
err := sh.WriteFile(ctx, "message.txt", []byte("Hello from Sh!"))
if err != nil {
log.Fatal(err)
}
buf, err := sh.ReadFile(ctx, "message.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(buf))
}
Output: Hello from Sh!
Example (ReadDir) ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "logs/error.log", []byte("")); err != nil {
log.Fatal(err)
}
if err := sh.WriteFile(ctx, "logs/access.log", []byte("")); err != nil {
log.Fatal(err)
}
for entry, err := range sh.ReadDir(ctx, "logs") {
if err != nil {
log.Fatal(err)
}
fmt.Println(entry.Name())
}
}
Output: access.log error.log
Example (Workflow) ¶
package main
import (
"context"
"fmt"
"log"
"strings"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine()).
Handle("tr", mem.Machine())
err := sh.WriteFile(ctx, "input/data.txt", []byte("Hello World"))
if err != nil {
log.Fatal(err)
}
input, err := sh.ReadFile(ctx, "input/data.txt")
if err != nil {
log.Fatal(err)
}
var buf strings.Builder
_, err = command.Copy(
&buf,
strings.NewReader(string(input)),
sh.NewStream(ctx, "tr", "A-Z", "a-z"),
)
if err != nil {
log.Fatal(err)
}
err = sh.WriteFile(ctx, "output/result.txt", []byte(buf.String()))
if err != nil {
log.Fatal(err)
}
result, err := sh.ReadFile(ctx, "output/result.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(result))
}
Output: hello world
func (*Sh) Glob ¶
Glob returns the names of all files matching pattern. Analogous to: io/fs.Glob, path.Match, glob, find, 9P walk.
The pattern syntax is the same as in path.Match. The pattern may describe hierarchical names such as usr/*/bin/ed.
Glob ignores file system errors such as I/O errors reading directories. The only possible returned error is path.ErrBadPattern, reporting that the pattern is malformed.
Requires: lesiw.io/fs.GlobFS || (lesiw.io/fs.StatFS && (lesiw.io/fs.ReadDirFS || lesiw.io/fs.WalkFS))
This is a convenience method that calls lesiw.io/fs.Glob.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "file1.txt", []byte("")); err != nil {
log.Fatal(err)
}
if err := sh.WriteFile(ctx, "file2.txt", []byte("")); err != nil {
log.Fatal(err)
}
if err := sh.WriteFile(ctx, "data.json", []byte("")); err != nil {
log.Fatal(err)
}
matches, err := sh.Glob(ctx, "*.txt")
if err != nil {
log.Fatal(err)
}
for _, match := range matches {
fmt.Println(match)
}
}
Output: ./file1.txt ./file2.txt
func (*Sh) Handle ¶
Handle registers a machine to handle the specified command. Returns the shell for method chaining.
func (*Sh) HandleFunc ¶
HandleFunc registers a function to handle the specified command. This is a convenience wrapper around Handle for function handlers. Returns the shell for method chaining.
func (*Sh) Localize ¶
Localize converts a path from Unix-style to native.
This is typically a lexical operation. For canonical path representation, use lesiw.io/fs.Abs.
Localize may be called with an already-localized path and should return the same path unchanged (idempotent behavior).
Requires: lesiw.io/fs.LocalizeFS
This is a convenience method that calls lesiw.io/fs.Localize.
func (*Sh) Lstat ¶
Lstat returns FileInfo describing the named file. Analogous to: os.Lstat, stat (without -L). If the file is a symbolic link, the returned FileInfo describes the symbolic link. Lstat makes no attempt to follow the link.
Requires: lesiw.io/fs.ReadLinkFS || lesiw.io/fs.StatFS
This is a convenience method that calls lesiw.io/fs.Lstat.
func (*Sh) Mkdir ¶
Mkdir creates a new directory. Analogous to: os.Mkdir, mkdir.
The directory mode is obtained from lesiw.io/fs.DirMode(ctx). If not set in the context, the default mode 0755 is used:
ctx = fs.WithDirMode(ctx, 0700) fs.Mkdir(ctx, fsys, "private") // Creates with mode 0700
Mkdir returns an error if the directory already exists or if the parent directory does not exist. Use lesiw.io/fs.MkdirAll to create parent directories automatically.
Requires: lesiw.io/fs.MkdirFS
This is a convenience method that calls lesiw.io/fs.Mkdir.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.Mkdir(ctx, "newdir"); err != nil {
log.Fatal(err)
}
for entry, err := range sh.ReadDir(ctx, ".") {
if err != nil {
log.Fatal(err)
}
fmt.Println(entry.Name())
}
}
Output: newdir
func (*Sh) MkdirAll ¶
MkdirAll creates a directory named name, along with any necessary parents. Analogous to: os.MkdirAll, mkdir -p.
The directory mode is obtained from lesiw.io/fs.DirMode(ctx). If not set in the context, the default mode 0755 is used:
ctx = fs.WithDirMode(ctx, 0700) fs.MkdirAll(ctx, fsys, "a/b/c") // All created with mode 0700
If name is already a directory, MkdirAll does nothing and returns nil.
Requires: lesiw.io/fs.MkdirAllFS || (lesiw.io/fs.MkdirFS && lesiw.io/fs.StatFS)
This is a convenience method that calls lesiw.io/fs.MkdirAll.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.MkdirAll(ctx, "a/b/c"); err != nil {
log.Fatal(err)
}
for entry, err := range sh.ReadDir(ctx, "a/b") {
if err != nil {
log.Fatal(err)
}
fmt.Println(entry.Name())
}
}
Output: c
func (*Sh) NewReader ¶
NewReader creates a read-only command that cancels on Close.
The command starts lazily on the first Read() call. Close() cancels the underlying context to immediately terminate the command, which is appropriate for read-only operations where the user has signaled they're done reading.
If Close() is called before any Read(), the command never starts.
This is a convenience method that calls NewReader.
func (*Sh) NewStream ¶
NewStream creates a bidirectional command stream with full Read/Write/Close access.
The returned io.ReadWriteCloser provides direct access to the command's stdin (Write), stdout (Read), and stdin close signal (Close).
If the underlying command does not support writing (is read-only), Write() will return an error. Close() closes stdin if supported, otherwise it is a no-op.
NewStream is primarily useful with command.Copy for pipeline composition. For most use cases, prefer NewReader (read-only with cancellation) or NewWriter (write-only with completion wait).
This is a convenience method that calls NewStream.
func (*Sh) NewWriter ¶
NewWriter creates a write-only command that waits for completion on Close.
The command starts lazily on the first Write() call. Close() waits for the command to complete gracefully by closing stdin and reading any output, which is appropriate for write-only operations that must finish processing before the operation is considered complete.
If Close() is called before any Write(), the command never starts.
NewWriter implements io.ReaderFrom for optimized copying. When io.Copy detects this, it will auto-close stdin after the source reaches EOF.
This is a convenience method that calls NewWriter.
func (*Sh) OS ¶
OS returns the operating system type for this shell. The value is detected only once on first use and cached for performance. Returns normalized GOOS values: "linux", "darwin", "freebsd", "openbsd", "netbsd", "dragonfly", "windows", or "unknown".
Example ¶
package main
import (
"context"
"fmt"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
// OS detection is cached after first call
os := sh.OS(ctx)
fmt.Println(os)
}
Output: linux
func (*Sh) Open ¶
Open opens the named file or directory for reading. Analogous to: io/fs.Open, os.Open, cat, tar, 9P Topen, S3 GetObject.
All paths use forward slashes (/) regardless of the operating system, following io/fs conventions. Use the lesiw.io/fs.path package (not path/filepath) for path manipulation. Implementations handle OS-specific conversion internally.
The returned lesiw.io/fs.ReadPathCloser must be closed when done. The Path() method returns the native filesystem path, or the input path if localization is not supported.
Files ¶
Returns a lesiw.io/fs.ReadPathCloser for reading the file contents.
Requires: lesiw.io/fs.FS
Directories ¶
A trailing slash returns a tar archive stream of the directory contents. A path identified as a directory via lesiw.io/fs.StatFS also returns a tar archive.
Requires: lesiw.io/fs.DirFS || (lesiw.io/fs.FS && (lesiw.io/fs.ReadDirFS || lesiw.io/fs.WalkFS))
This is a convenience method that calls lesiw.io/fs.Open.
Example ¶
package main
import (
"context"
"fmt"
"io"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "file.txt", []byte("hello")); err != nil {
log.Fatal(err)
}
f, err := sh.Open(ctx, "file.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
Output: hello
func (*Sh) OpenBuffer ¶
OpenBuffer returns a lazy-executing reader for the file at name. The file is opened on first Read(), not when OpenBuffer is called. Errors from opening the file are returned from Read(), not OpenBuffer().
Use OpenBuffer when you only need to read bytes and don't need File metadata (Path, Stat, etc.). For metadata access, use Open instead.
Example:
io.Copy(dst, fs.OpenBuffer(ctx, fsys, "input.txt"))
This is a convenience method that calls lesiw.io/fs.OpenBuffer.
func (*Sh) Read ¶
Read executes a command and returns its output as a string. All trailing whitespace is stripped from the output. For exact output, use io.ReadAll.
If the command fails, the error will contain an exit code and log output.
This is a convenience method that calls Read.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine(), "echo")
out, err := sh.Read(ctx, "echo", "hello world")
if err != nil {
log.Fatal(err)
}
fmt.Println(out)
}
Output: hello world
func (*Sh) ReadDir ¶
ReadDir reads the named directory and returns an iterator over its entries. Analogous to: os.ReadDir, io/fs.ReadDir, ls, 9P Tread on directory.
Requires: lesiw.io/fs.ReadDirFS || lesiw.io/fs.WalkFS
This is a convenience method that calls lesiw.io/fs.ReadDir.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "files/a.txt", []byte("")); err != nil {
log.Fatal(err)
}
if err := sh.WriteFile(ctx, "files/b.txt", []byte("")); err != nil {
log.Fatal(err)
}
for entry, err := range sh.ReadDir(ctx, "files") {
if err != nil {
log.Fatal(err)
}
fmt.Println(entry.Name())
}
}
Output: a.txt b.txt
func (*Sh) ReadFile ¶
ReadFile reads the named file and returns its contents. Analogous to: io/fs.ReadFile, os.ReadFile, cat.
Requires: lesiw.io/fs.FS
This is a convenience method that calls lesiw.io/fs.ReadFile.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "data.txt", []byte("content")); err != nil {
log.Fatal(err)
}
data, err := sh.ReadFile(ctx, "data.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
Output: content
func (*Sh) ReadLink ¶
ReadLink returns the destination of the named symbolic link. Analogous to: os.Readlink, readlink, 9P2000.u Treadlink. If the link destination is relative, ReadLink returns the relative path without resolving it to an absolute one.
Requires: lesiw.io/fs.ReadLinkFS
This is a convenience method that calls lesiw.io/fs.ReadLink.
func (*Sh) Remove ¶
Remove removes the named file or empty directory. Analogous to: os.Remove, rm, 9P Tremove, S3 DeleteObject. Returns an error if the file does not exist or if a directory is not empty.
Requires: lesiw.io/fs.RemoveFS
This is a convenience method that calls lesiw.io/fs.Remove.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "file.txt", []byte("content")); err != nil {
log.Fatal(err)
}
if err := sh.Remove(ctx, "file.txt"); err != nil {
log.Fatal(err)
}
var n int
for _, err := range sh.ReadDir(ctx, ".") {
if err != nil {
log.Fatal(err)
}
n++
}
if n == 0 {
fmt.Println("(empty)")
}
}
Output: (empty)
func (*Sh) RemoveAll ¶
RemoveAll removes name and any children it contains. Analogous to: os.RemoveAll, rm -rf.
Requires: lesiw.io/fs.RemoveAllFS || (lesiw.io/fs.RemoveFS && lesiw.io/fs.StatFS && (lesiw.io/fs.ReadDirFS || lesiw.io/fs.WalkFS))
This is a convenience method that calls lesiw.io/fs.RemoveAll.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.MkdirAll(ctx, "dir/subdir"); err != nil {
log.Fatal(err)
}
err := sh.WriteFile(ctx, "dir/file.txt", []byte("content"))
if err != nil {
log.Fatal(err)
}
if err := sh.RemoveAll(ctx, "dir"); err != nil {
log.Fatal(err)
}
var n int
for _, err := range sh.ReadDir(ctx, ".") {
if err != nil {
log.Fatal(err)
}
n++
}
if n == 0 {
fmt.Println("(empty)")
}
}
Output: (empty)
func (*Sh) Rename ¶
Rename renames (moves) oldname to newname. Analogous to: os.Rename, mv, 9P2000.u Trename. If newname already exists and is not a directory, Rename replaces it.
Requires: lesiw.io/fs.RenameFS || (lesiw.io/fs.FS && lesiw.io/fs.CreateFS && lesiw.io/fs.RemoveFS)
This is a convenience method that calls lesiw.io/fs.Rename.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "old.txt", []byte("data")); err != nil {
log.Fatal(err)
}
if err := sh.Rename(ctx, "old.txt", "new.txt"); err != nil {
log.Fatal(err)
}
content, err := sh.ReadFile(ctx, "new.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
}
Output: data
func (*Sh) Shutdown ¶
Shutdown shuts down the machine if it implements ShutdownMachine. Returns nil if the machine does not implement ShutdownMachine.
The context passed to Shutdown is derived using context.WithoutCancel to ensure cleanup can complete even after the parent context is canceled.
This is a convenience method that calls Shutdown.
func (*Sh) Stat ¶
Stat returns file metadata for the named file. Analogous to: io/fs.Stat, os.Stat, stat, ls -l, 9P Tstat, S3 HeadObject.
Requires: lesiw.io/fs.StatFS
This is a convenience method that calls lesiw.io/fs.Stat.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "test.txt", []byte("hello")); err != nil {
log.Fatal(err)
}
info, err := sh.Stat(ctx, "test.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(info.Name())
}
Output: test.txt
func (*Sh) Symlink ¶
Symlink creates newname as a symbolic link to oldname. Analogous to: os.Symlink, ln -s, 9P2000.u Tsymlink.
Requires: lesiw.io/fs.SymlinkFS
This is a convenience method that calls lesiw.io/fs.Symlink.
func (*Sh) Temp ¶
Temp creates a temporary file or directory. Analogous to: os.CreateTemp, os.MkdirTemp, mktemp.
The returned lesiw.io/fs.WritePathCloser must be closed when done. Path() returns the full path to the created resource. The caller is responsible for removing the temporary resource when done (typically with lesiw.io/fs.RemoveAll).
Files ¶
Without a trailing separator, creates a temporary file. The name parameter serves as a prefix or pattern (implementation-specific). The file name will typically have the pattern: name-randomhex
Requires: lesiw.io/fs.TempFS || lesiw.io/fs.TempDirFS || lesiw.io/fs.CreateFS
Directories ¶
With a trailing separator, creates a temporary directory and returns a tar stream writer for extracting files into it. The directory name will typically have the pattern: name-randomhex
Requires: lesiw.io/fs.TempDirFS || lesiw.io/fs.MkdirFS
This is a convenience method that calls lesiw.io/fs.Temp.
func (*Sh) Truncate ¶
Truncate changes the size of the named file or empties a directory. Analogous to: os.Truncate, truncate, 9P Twstat.
Like os.Truncate, Truncate returns an error if the path doesn't exist. If lesiw.io/fs.StatFS is available, the existence check happens before attempting the operation. Otherwise, the error comes from the truncate operation itself.
Files ¶
If the file is larger than size, it is truncated. If it is smaller, it is extended with zeros.
Requires: lesiw.io/fs.TruncateFS || (lesiw.io/fs.FS && lesiw.io/fs.RemoveFS && lesiw.io/fs.CreateFS)
Directories ¶
A trailing slash indicates a directory. Removes all contents, leaving an empty directory.
Requires: lesiw.io/fs.TruncateDirFS || (lesiw.io/fs.RemoveAllFS && lesiw.io/fs.MkdirFS)
This is a convenience method that calls lesiw.io/fs.Truncate.
func (*Sh) Unshell ¶
Unshell implements the Unsheller interface. It returns the machine one layer down (the inner/core machine that was wrapped by Shell).
This allows selective command whitelisting by explicitly routing specific commands to the underlying machine:
sh := command.Shell(sys.Machine())
sh = sh.Handle("go", sh.Unshell()) // Whitelist "go" command
IMPORTANT: This breaks Shell's portability guarantees. The returned machine will execute commands on the underlying machine even if they aren't explicitly registered in the Shell.
Example ¶
package main
import (
"context"
"fmt"
"log"
"strings"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx := context.Background()
sh := command.Shell(mem.Machine())
sh = sh.Handle("tr", sh.Unshell())
var buf strings.Builder
_, err := command.Copy(
&buf,
strings.NewReader("hello"),
sh.NewStream(ctx, "tr", "a-z", "A-Z"),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
}
Output: HELLO
func (*Sh) Walk ¶
Walk traverses the filesystem rooted at root. Analogous to: io/fs.WalkDir, find, tree.
The depth parameter controls how deep to traverse (like find -maxdepth):
- depth <= 0: unlimited depth (like find without -maxdepth)
- depth >= 1: root directory plus n-1 levels of subdirectories (like find -maxdepth n)
Walk does not guarantee any particular order (lexicographic or breadth-first). Implementations may choose whatever order is most efficient. For guaranteed lexicographic order within each directory, use lesiw.io/fs.ReadDir.
Walk does not follow symbolic links. Entries are yielded for symbolic links themselves, but they are not traversed.
Entries returned by Walk have Path() populated with the full path.
If an error occurs reading a directory, the iteration yields a zero DirEntry and the error. The caller can choose to continue iterating (skip that directory) or break to stop the walk.
Requires: lesiw.io/fs.WalkFS || lesiw.io/fs.ReadDirFS
This is a convenience method that calls lesiw.io/fs.Walk.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
if err := sh.WriteFile(ctx, "a/file1.txt", []byte("")); err != nil {
log.Fatal(err)
}
if err := sh.WriteFile(ctx, "a/b/file2.txt", []byte("")); err != nil {
log.Fatal(err)
}
for entry, err := range sh.Walk(ctx, "a", -1) {
if err != nil {
log.Fatal(err)
}
fmt.Println(entry.Name())
}
}
Output: b file1.txt file2.txt
func (*Sh) WriteFile ¶
WriteFile writes data to the named file in the filesystem. It creates the file or truncates it if it already exists. The file is created with mode 0644 (or the mode specified via WithFileMode).
Like os.WriteFile, this always truncates existing files to zero length before writing.
If the parent directory does not exist and the filesystem implements MkdirFS, WriteFile automatically creates the parent directories with mode 0755 (or the mode specified via WithDirMode).
This is analogous to os.WriteFile and io/fs.ReadFile.
Requires: lesiw.io/fs.CreateFS
This is a convenience method that calls lesiw.io/fs.WriteFile.
Example ¶
package main
import (
"context"
"fmt"
"log"
"lesiw.io/command"
"lesiw.io/command/mem"
)
func main() {
ctx, sh := context.Background(), command.Shell(mem.Machine())
err := sh.WriteFile(ctx, "message.txt", []byte("Hello from Sh!"))
if err != nil {
log.Fatal(err)
}
content, err := sh.ReadFile(ctx, "message.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
}
Output: Hello from Sh!
type ShutdownMachine ¶
type ShutdownMachine interface {
Machine
// Shutdown releases any resources held by the machine.
//
// The context passed to Shutdown may lack cancelation, as the
// command.Shutdown helper derives a context using context.WithoutCancel
// to ensure cleanup can complete even after the parent context is
// canceled.
//
// Because the context may not be canceled, implementations must ensure
// all operations performed by Shutdown do not block indefinitely by
// deriving their own context with an appropriate timeout.
Shutdown(ctx context.Context) error
}
ShutdownMachine is an optional interface that allows a Machine to provide cleanup functionality.
When a Machine implements ShutdownMachine, the command.Shutdown() function will call Shutdown(ctx) to clean up resources.
This is useful for machines that hold resources like network connections or container instances that need explicit cleanup.
type Unsheller ¶
type Unsheller interface {
// Unshell returns a Machine that falls back to inner layers when
// commands aren't found.
//
// If the machine cannot provide fallback behavior, or if exposing
// internals doesn't make sense, Unshell returns nil.
Unshell() Machine
}
Unsheller is an optional interface that machines can implement to expose a version of themselves with fallback routing to inner machines.
This is the opposite of Shell: while Shell creates explicit routing with "command not found" for unregistered commands, Unshell removes that protection and allows fallback to inner machine implementations.
IMPORTANT: Unshell breaks portability guarantees. Code using Unshell may depend on commands available in the underlying machine that aren't explicitly registered. Use Unshell only when you understand the tradeoffs, such as for probing operations (OS, Arch, FS) where you want to query the actual underlying system.
This is analogous to "unsafe" in memory management - it removes safety guarantees but enables necessary low-level operations.
type WriteBuffer ¶
type WriteBuffer interface {
Buffer
// Write writes data to the command's stdin.
// The command starts on first Write if it hasn't started from Read.
// Implementations should buffer or stream data to the command's stdin.
io.Writer
// Close closes the command's stdin, signaling EOF to the command.
// This does NOT close stdout - the buffer must still be read to EOF.
// After Close, subsequent Write calls must return an error.
// Implementations should wait for stdin close to propagate to the command.
io.Closer
}
WriteBuffer is an optional interface for buffers that accept input. Buffers that accept input allow writing to the command's stdin.
Note that Close closes stdin, not stdout. The buffer must still be read to EOF to observe command completion.
Source Files
¶
- buffer.go
- copy.go
- crlf.go
- doc.go
- env.go
- error.go
- fail.go
- fs.go
- fs_abs.go
- fs_append.go
- fs_append_dir.go
- fs_chmod.go
- fs_chown.go
- fs_chtimes.go
- fs_create.go
- fs_localize.go
- fs_mkdir.go
- fs_mkdir_all.go
- fs_open.go
- fs_open_dir.go
- fs_remove.go
- fs_remove_all.go
- fs_rename.go
- fs_stat.go
- fs_temp.go
- fs_truncate.go
- fs_types.go
- fs_walk.go
- fs_walk_dos.go
- fs_walk_posix.go
- fs_walk_win.go
- io.go
- machine.go
- os.go
- reader.go
- sh.go
- sh_fs.go
- sh_machine.go
- stream.go
- string.go
- unshell.go
- windows.go
- writer.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package ctr implements a command.Machine that executes commands in containers using docker, podman, nerdctl, or lima nerdctl.
|
Package ctr implements a command.Machine that executes commands in containers using docker, podman, nerdctl, or lima nerdctl. |
|
internal
|
|
|
generate/sh
command
Package main generates Sh convenience methods that delegate to package-level helper functions.
|
Package main generates Sh convenience methods that delegate to package-level helper functions. |
|
Package mem provides an in-memory command.Machine for tests and examples.
|
Package mem provides an in-memory command.Machine for tests and examples. |
|
Package mock provides a Machine implementation for testing that tracks invocations and allows queuing responses.
|
Package mock provides a Machine implementation for testing that tracks invocations and allows queuing responses. |
|
Package ssh implements a command.Machine that executes commands over SSH.
|
Package ssh implements a command.Machine that executes commands over SSH. |
|
Package sub implements a command.Machine that prefixes all commands with a fixed set of arguments.
|
Package sub implements a command.Machine that prefixes all commands with a fixed set of arguments. |
|
Package sys implements a command.Machine that executes commands on the local system using os/exec.
|
Package sys implements a command.Machine that executes commands on the local system using os/exec. |