command

package module
v0.0.0-...-9b5a01e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 2, 2026 License: BSD-3-Clause Imports: 22 Imported by: 0

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:

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

Examples

Constants

This section is empty.

Variables

View Source
var (
	Trace   = io.Discard
	ShTrace = prefix.NewWriter("+ ", stderr)
)
View Source
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.

View Source
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

func Arch(ctx context.Context, m Machine) string

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

func Attach(buf Buffer) error

Attach attaches buf to the controlling terminal if it implements AttachBuffer. Does nothing if buf does not implement AttachBuffer.

func Copy

func Copy(
	dst io.Writer, src io.Reader, mid ...io.ReadWriter,
) (written int64, err error)

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

func Do(ctx context.Context, m Machine, args ...string) error

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

func Env(ctx context.Context, m Machine, key string) string

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 Envs

func Envs(ctx context.Context) map[string]string

Envs returns a map of the environment variables stored in ctx.

func Exec

func Exec(ctx context.Context, m Machine, args ...string) error

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

func FS(m Machine) fs.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

func Log(buf Buffer, w io.Writer)

Log sets the log destination for buf if it implements LogBuffer. Does nothing if buf does not implement LogBuffer.

func NewReader

func NewReader(ctx context.Context, m Machine, args ...string) io.ReadCloser

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

func NewStream(
	ctx context.Context, m Machine, args ...string,
) io.ReadWriteCloser

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

func NewWriter(ctx context.Context, m Machine, args ...string) io.WriteCloser

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

func NotFound(err error) bool

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

func OS(ctx context.Context, m Machine) string

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

func Read(ctx context.Context, m Machine, args ...string) (string, error)

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

func Shutdown(ctx context.Context, m Machine) error

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

func String(buf Buffer) 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 UnsetEnv

func UnsetEnv(ctx context.Context, name string) context.Context

UnsetEnv returns a new context with the named environment variable removed.

func WithEnv

func WithEnv(ctx context.Context, env map[string]string) context.Context

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

func WithoutEnv(ctx context.Context) context.Context

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:

func Fail

func Fail(err error) Buffer

Fail returns a Buffer that returns err on all Read operations.

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.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type FSMachine

type FSMachine interface {
	Machine

	// FS returns a fs.FS for this Machine.
	FS() fs.FS
}

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:

func Handle

func Handle(m Machine, command string, handler Machine) Machine

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

func HandleFunc(
	m Machine,
	command string,
	fn func(context.Context, ...string) Buffer,
) Machine

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

func Unshell(m Machine) Machine

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

type MachineFunc func(context.Context, ...string) Buffer

MachineFunc is an adapter to allow ordinary functions to be used as Machines. This is similar to http.HandlerFunc.

func (MachineFunc) Command

func (f MachineFunc) Command(ctx context.Context, args ...string) Buffer

Command implements the Machine interface.

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

func Shell(core Machine, commands ...string) *Sh

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

func (sh *Sh) Abs(
	ctx context.Context, name string,
) (string, error)

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

func (sh *Sh) Append(
	ctx context.Context, name string,
) (fs.WritePathCloser, error)

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

func (sh *Sh) AppendBuffer(
	ctx context.Context, name string,
) io.WriteCloser

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

func (sh *Sh) Arch(ctx context.Context) string

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

func (sh *Sh) Chmod(
	ctx context.Context, name string, mode fs.Mode,
) error

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

func (sh *Sh) Chown(
	ctx context.Context, name string, uid int, gid int,
) error

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

func (sh *Sh) Chtimes(
	ctx context.Context, name string, atime time.Time, mtime time.Time,
) error

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

func (sh *Sh) Close() error

Close closes a filesystem if it implements io.Closer.

This is a convenience method that calls lesiw.io/fs.Close.

func (*Sh) Command

func (sh *Sh) Command(ctx context.Context, args ...string) Buffer

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

func (sh *Sh) Create(
	ctx context.Context, name string,
) (fs.WritePathCloser, error)

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

func (sh *Sh) CreateBuffer(
	ctx context.Context, name string,
) io.WriteCloser

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

func (sh *Sh) Do(
	ctx context.Context, args ...string,
) error

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

func (sh *Sh) Env(ctx context.Context, key string) string

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

func (sh *Sh) Exec(
	ctx context.Context, args ...string,
) error

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

func (sh *Sh) FS() fs.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

func (sh *Sh) Glob(
	ctx context.Context, pattern string,
) ([]string, error)

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

func (sh *Sh) Handle(command string, machine Machine) *Sh

Handle registers a machine to handle the specified command. Returns the shell for method chaining.

func (*Sh) HandleFunc

func (sh *Sh) HandleFunc(
	command string,
	fn func(context.Context, ...string) Buffer,
) *Sh

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

func (sh *Sh) Localize(
	ctx context.Context, path string,
) (string, error)

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

func (sh *Sh) Lstat(
	ctx context.Context, name string,
) (fs.FileInfo, error)

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

func (sh *Sh) Mkdir(
	ctx context.Context, name string,
) error

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

func (sh *Sh) MkdirAll(
	ctx context.Context, name string,
) error

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

func (sh *Sh) NewReader(
	ctx context.Context, args ...string,
) io.ReadCloser

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

func (sh *Sh) NewStream(
	ctx context.Context, args ...string,
) io.ReadWriteCloser

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

func (sh *Sh) NewWriter(
	ctx context.Context, args ...string,
) io.WriteCloser

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

func (sh *Sh) OS(ctx context.Context) string

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

func (sh *Sh) Open(
	ctx context.Context, name string,
) (fs.ReadPathCloser, error)

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

func (sh *Sh) OpenBuffer(
	ctx context.Context, name string,
) io.ReadCloser

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

func (sh *Sh) Read(
	ctx context.Context, args ...string,
) (string, error)

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

func (sh *Sh) ReadDir(
	ctx context.Context, name string,
) iter.Seq2[fs.DirEntry, error]

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

func (sh *Sh) ReadFile(
	ctx context.Context, name string,
) ([]byte, error)

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 *Sh) ReadLink(
	ctx context.Context, name string,
) (string, error)

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

func (sh *Sh) Remove(
	ctx context.Context, name string,
) error

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

func (sh *Sh) RemoveAll(
	ctx context.Context, name string,
) error

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

func (sh *Sh) Rename(
	ctx context.Context, oldname string, newname string,
) error

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

func (sh *Sh) Shutdown(
	ctx context.Context,
) error

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

func (sh *Sh) Stat(
	ctx context.Context, name string,
) (fs.FileInfo, error)

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 *Sh) Symlink(
	ctx context.Context, oldname string, newname string,
) error

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

func (sh *Sh) Temp(
	ctx context.Context, name string,
) (fs.WritePathCloser, error)

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

func (sh *Sh) Truncate(
	ctx context.Context, name string, size int64,
) error

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

func (sh *Sh) Unshell() Machine

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

func (sh *Sh) Walk(
	ctx context.Context, root string, depth int,
) iter.Seq2[fs.DirEntry, error]

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

func (sh *Sh) WriteFile(
	ctx context.Context, name string, data []byte,
) error

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.

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.
sh
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL