Documentation
¶
Overview ¶
Package invoke provides a unified interface for command execution and file transfer across Local, SSH, and Docker environments.
Core Interfaces ¶
- Environment: The connection to a system. Swap the provider to switch targets.
- Process: A handle to a running command (Wait, Signal, Close).
Streaming ¶
invoke is streaming-first. Output is not buffered by default — attach an io.Writer to your Command. For convenience, use Executor.RunBuffered to capture output.
Error Model ¶
- ExitError: The command ran but exited non-zero. Contains exit code and stderr.
- TransportError: The underlying transport failed (connection lost, daemon unreachable). Retryable via WithRetry.
Privilege Escalation ¶
Supported via WithSudo. Uses sudo -n for non-interactive execution.
Example (SshConfigReader) ¶
package main
import (
"fmt"
"log"
"strings"
"github.com/ruffel/invoke/providers/ssh"
)
func main() {
// Example of loading SSH config from a string (or file)
configContent := `
Host prod-db
HostName 10.0.0.5
User admin
Port 2222
IdentityFile ~/.ssh/prod_key.pem
StrictHostKeyChecking no
`
// Parse the config
cfg, err := ssh.NewFromSSHConfigReader("prod-db", strings.NewReader(configContent))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Host: %s\n", cfg.Host)
fmt.Printf("User: %s\n", cfg.User)
fmt.Printf("Port: %d\n", cfg.Port)
}
Output: Host: 10.0.0.5 User: admin Port: 2222
Index ¶
- Variables
- type BufferedResult
- type Command
- type Environment
- type ExecConfig
- type ExecOption
- type Executor
- func (e *Executor) Download(ctx context.Context, remotePath, localPath string, opts ...FileOption) error
- func (e *Executor) LookPath(ctx context.Context, file string) (string, error)
- func (e *Executor) Run(ctx context.Context, cmd *Command, opts ...ExecOption) (*Result, error)
- func (e *Executor) RunBuffered(ctx context.Context, cmd *Command, opts ...ExecOption) (*BufferedResult, error)
- func (e *Executor) RunInteractiveTTY(ctx context.Context, cmd *Command, opts ...ExecOption) (*Result, error)
- func (e *Executor) RunLineStream(ctx context.Context, cmd *Command, onLine func(string)) error
- func (e *Executor) RunShell(ctx context.Context, cmdStr string, opts ...ExecOption) (*BufferedResult, error)
- func (e *Executor) Start(ctx context.Context, cmd *Command) (Process, error)
- func (e *Executor) TargetOS() TargetOS
- func (e *Executor) Upload(ctx context.Context, localPath, remotePath string, opts ...FileOption) error
- type ExitError
- type FileConfig
- type FileOption
- type Process
- type ProgressFunc
- type Result
- type SudoConfig
- type SudoOption
- type TargetOS
- type TransportError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrEnvironmentClosed = errors.New("environment is closed")
ErrEnvironmentClosed indicates that an operation was attempted on a closed environment.
var ErrNotSupported = errors.New("operation not supported")
ErrNotSupported indicates that the requested feature (e.g., TTY) is not supported by the specific provider or OS.
Functions ¶
This section is empty.
Types ¶
type BufferedResult ¶
type BufferedResult struct {
Result
Stdout []byte // Captured standard output
Stderr []byte // Captured standard error
}
BufferedResult extends Result with captured stdout and stderr content. Returned by Executor.RunBuffered and Executor.RunShell.
type Command ¶
type Command struct {
Cmd string // Binary name or path to executable
Args []string // Arguments to pass to the binary
Env []string // Environment variables in "KEY=VALUE" format
Dir string // Working directory for execution
// Standard streams. If nil, defaults to empty/discard.
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
// Tty allocates a PTY. Useful for interactive commands (e.g. sudo).
Tty bool
}
Command configures a process execution.
func NewCommand ¶
NewCommand creates a new Command with the given binary and arguments.
func ParseCommand ¶
ParseCommand parses a shell command string into a Command struct using shlex. It handles quoted arguments correctly.
func ShellCommand ¶ added in v0.1.0
ShellCommand constructs a command that runs the provided script inside the system shell. Equivalent to: sh -c "<script>".
Use this for one-liners, pipelines, and glob expansion:
cmd := invoke.ShellCommand("ls -la | grep foo")
type Environment ¶
type Environment interface {
io.Closer
// Run executes a command synchronously.
// Returns the result (exit code, duration) and any error.
// Output is not captured by default; attach writers to Command.Stdout/Stderr.
Run(ctx context.Context, cmd *Command) (*Result, error)
// Start initiates a command asynchronously.
// The caller manages the returned Process (Wait/Signal) and must ensure resources
// are released via Wait() or Close().
Start(ctx context.Context, cmd *Command) (Process, error)
// TargetOS returns the operating system of the target environment.
TargetOS() TargetOS
// Upload copies a local file or directory to the remote destination.
// Missing parent directories at the destination are created automatically.
Upload(ctx context.Context, localPath, remotePath string, opts ...FileOption) error
// Download copies a remote file or directory to the local destination.
// Missing parent directories at the local destination are created automatically.
Download(ctx context.Context, remotePath, localPath string, opts ...FileOption) error
// LookPath searches for an executable named file in the PATH of the target environment.
LookPath(ctx context.Context, file string) (string, error)
}
Environment abstracts the underlying system where commands are executed (e.g., Local, SSH, Docker).
Example (Upload) ¶
package main
import (
"context"
"log"
"time"
"github.com/ruffel/invoke"
"github.com/ruffel/invoke/providers/mock"
testifymock "github.com/stretchr/testify/mock"
)
func main() {
// Demonstrating the FileTransfer interface usage (now part of Environment)
var env invoke.Environment = mock.New()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Setup mock expectation
env.(*mock.Environment).On("Upload", ctx, "./config.json", "/etc/app/config.json", testifymock.Anything).Return(nil)
// Upload with permissions override
err := env.Upload(ctx, "./config.json", "/etc/app/config.json",
invoke.WithPermissions(0o600),
)
if err != nil {
log.Printf("Upload failed: %v", err)
}
}
Output:
Example (Upload_download) ¶
package main
import (
"context"
"fmt"
"os"
"github.com/ruffel/invoke"
"github.com/ruffel/invoke/providers/local"
)
func main() {
env, err := local.New()
if err != nil {
panic(err)
}
_ = os.WriteFile("localfile.txt", []byte("hello world"), 0o600)
defer func() { _ = os.Remove("localfile.txt") }()
defer func() { _ = os.Remove("localfile.bak") }()
ctx := context.Background()
err = env.Upload(ctx, "localfile.txt", "/tmp/localfile.txt", invoke.WithPermissions(0o644))
if err != nil {
panic(err)
}
err = env.Download(ctx, "/tmp/localfile.txt", "localfile.bak")
if err != nil {
panic(err)
}
content, err := os.ReadFile("localfile.bak")
if err != nil {
panic(err)
}
fmt.Printf("Downloaded content: %s\n", string(content))
}
Output: Downloaded content: hello world
Example (WithProgress) ¶
package main
import (
"context"
"fmt"
"os"
"github.com/ruffel/invoke"
"github.com/ruffel/invoke/providers/local"
)
func main() {
env, err := local.New()
if err != nil {
panic(err)
}
_ = os.WriteFile("largefile.dat", []byte("1234567890"), 0o600)
defer func() { _ = os.Remove("largefile.dat") }()
defer func() { _ = os.Remove("largefile.dat.bak") }()
ctx := context.Background()
err = env.Upload(ctx, "largefile.dat", "largefile.dat.bak",
invoke.WithProgress(func(current, total int64) {
fmt.Printf("Transferred %d/%d bytes\n", current, total)
}),
)
if err != nil {
panic(err)
}
}
Output: Transferred 10/10 bytes
type ExecConfig ¶
type ExecConfig struct {
SudoConfig *SudoConfig
RetryAttempts int
RetryDelay time.Duration
}
ExecConfig holds configuration derived from options.
type ExecOption ¶
type ExecOption func(*ExecConfig)
ExecOption defines a functional option for execution.
type Executor ¶
type Executor struct {
// contains filtered or unexported fields
}
Executor handles command execution with retry logic, sudo support, and output buffering.
func NewExecutor ¶
func NewExecutor(env Environment) *Executor
NewExecutor creates a new Executor with the given environment.
func (*Executor) Download ¶
func (e *Executor) Download(ctx context.Context, remotePath, localPath string, opts ...FileOption) error
Download copies a remote file or directory to the local destination. It delegates directly to the underlying Environment.
func (*Executor) LookPath ¶
LookPath resolves an executable path using the underlying environment's LookPath strategy.
func (*Executor) Run ¶
Run executes a command, respecting context cancellation and configured retry policies.
Retry behavior:
- Success (exit code 0) returns immediately.
- Non-zero exit codes and *ExitError are terminal and never retried.
- context.Canceled, context.DeadlineExceeded, ErrNotSupported, and ErrEnvironmentClosed are terminal and never retried.
- Other errors are treated as transport-like and retried when attempts remain.
Example (Sudo) ¶
package main
import (
"context"
"fmt"
"github.com/ruffel/invoke"
"github.com/ruffel/invoke/providers/mock"
testifymock "github.com/stretchr/testify/mock"
)
func main() {
// Example of using high-level options
env := mock.New() // Using mock for safety in example
// Mock the result
// Match any command that has the right string components, ignoring pointers (Stdout/Stderr)
matcher := testifymock.MatchedBy(func(c *invoke.Command) bool {
return c.Cmd == "sudo" && len(c.Args) == 4 && c.Args[3] == "/root"
})
env.On("Run", context.Background(), matcher).Run(func(args testifymock.Arguments) {
cmd := args.Get(1).(*invoke.Command)
if cmd.Stdout != nil {
_, _ = fmt.Fprint(cmd.Stdout, "secret.txt\n")
}
}).Return(&invoke.Result{ExitCode: 0}, nil)
exec := invoke.NewExecutor(env)
ctx := context.Background()
// WithSudo() automatically wraps the command
cmd := invoke.Command{Cmd: "ls", Args: []string{"/root"}}
// We use RunBuffered to capture output
res, err := exec.RunBuffered(ctx, &cmd, invoke.WithSudo())
if err != nil {
panic(err)
}
fmt.Printf("Sudo Output: %s", res.Stdout)
}
Output: Sudo Output: secret.txt
func (*Executor) RunBuffered ¶
func (e *Executor) RunBuffered(ctx context.Context, cmd *Command, opts ...ExecOption) (*BufferedResult, error)
RunBuffered executes a command and captures both Stdout and Stderr into byte slices.
It creates a shallow copy of cmd and overrides Stdout/Stderr with internal buffers. The caller's original Command is not mutated. If you need simultaneous streaming and buffering, compose with io.MultiWriter on the Command before calling Run directly. Output is held in memory, so prefer streaming for commands with large or unbounded output.
Example (Local) ¶
package main
import (
"context"
"fmt"
"github.com/ruffel/invoke"
"github.com/ruffel/invoke/providers/local"
)
func main() {
env, err := local.New()
if err != nil {
panic(err)
}
defer func() { _ = env.Close() }()
exec := invoke.NewExecutor(env)
cmd := invoke.Command{
Cmd: "echo",
Args: []string{"hello", "world"},
}
ctx := context.Background()
res, err := exec.RunBuffered(ctx, &cmd)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", res.Stdout)
}
Output: hello world
func (*Executor) RunInteractiveTTY ¶ added in v0.1.0
func (e *Executor) RunInteractiveTTY(ctx context.Context, cmd *Command, opts ...ExecOption) (*Result, error)
RunInteractiveTTY executes a command in interactive TTY mode.
If Stdin/Stdout/Stderr are nil they default to os.Stdin/os.Stdout/os.Stderr. This behavior is specific to RunInteractiveTTY and overrides the default Command behavior, where nil streams typically default to empty/discard.
When reading from the current terminal, stdin is switched to raw mode for the duration of the command. The original terminal state is restored via defer, ensuring cleanup even if the command panics.
func (*Executor) RunLineStream ¶
RunLineStream streams stdout line-by-line to onLine.
It creates a shallow copy of cmd and overrides Stdout with an internal pipe. The caller's original Command is not mutated. Useful for live logging of build output or deployment progress. Stderr is not streamed by this helper, and lines are subject to bufio.Scanner's token size limit.
func (*Executor) RunShell ¶
func (e *Executor) RunShell(ctx context.Context, cmdStr string, opts ...ExecOption) (*BufferedResult, error)
RunShell executes a shell command string using the target OS's default shell.
func (*Executor) Start ¶
Start initiates a command asynchronously. Caller is responsible for Process.Wait().
type ExitError ¶
type ExitError struct {
Command *Command
ExitCode int
Stderr []byte // Populated by RunBuffered for convenience
Cause error // Underlying error from the OS/transport layer
}
ExitError represents a command that ran but exited with a non-zero code.
When Wait() returns an ExitError, the accompanying *Result is still populated with the exit code and duration. Use errors.As to extract the exit code:
var exitErr *invoke.ExitError
if errors.As(err, &exitErr) {
fmt.Println("exit code:", exitErr.ExitCode)
}
type FileConfig ¶
type FileConfig struct {
Permissions os.FileMode // Destination perms override (0 means preserve/default)
Recursive bool // Default true for generic uploads
Progress ProgressFunc
}
FileConfig holds configuration for file transfers.
type FileOption ¶
type FileOption func(*FileConfig)
FileOption defines a functional option for file transfers.
func WithPermissions ¶
func WithPermissions(mode os.FileMode) FileOption
WithPermissions forces specific destination file mode.
func WithProgress ¶
func WithProgress(fn ProgressFunc) FileOption
WithProgress calls fn with progress updates.
func WithRecursive ¶ added in v0.1.0
func WithRecursive(recursive bool) FileOption
WithRecursive enables or disables recursive directory transfers.
type Process ¶
type Process interface {
io.Closer
// Wait blocks until the process exits and returns the result.
//
// On success (exit code 0), returns (*Result, nil).
// On non-zero exit, returns (*Result, *ExitError) — the Result is always
// populated with the exit code and duration, even when an error is returned.
//
// Wait is idempotent: calling it multiple times returns the same result.
Wait() (*Result, error)
// Signal sends an OS signal to the process.
// Support for specific signals depends on the underlying provider.
Signal(sig os.Signal) error
}
Process represents a command that has been started but not yet completed.
type ProgressFunc ¶
type ProgressFunc func(current, total int64)
ProgressFunc is a callback for tracking file transfer progress.
type Result ¶
type Result struct {
ExitCode int // Process exit code (0 indicates success)
Duration time.Duration // Wall-clock time from start to exit
Error error // Provider-level wait/transport error. Prefer the returned error for control flow.
}
Result contains metadata about a completed command execution. Always populated by Wait(), even when Wait returns an error.
type SudoConfig ¶ added in v0.0.3
type SudoConfig struct {
User string // Target user (-u)
Group string // Target group (-g)
PreserveEnv bool // Preserve environment (-E)
CustomFlags []string // Additional flags
}
SudoConfig defines advanced privilege escalation options.
type SudoOption ¶ added in v0.0.3
type SudoOption func(*SudoConfig)
SudoOption defines a functional option for sudo configuration.
func WithSudoCustomFlags ¶ added in v0.1.0
func WithSudoCustomFlags(flags ...string) SudoOption
WithSudoCustomFlags adds additional sudo flags.
func WithSudoGroup ¶ added in v0.1.0
func WithSudoGroup(group string) SudoOption
WithSudoGroup sets the target group.
func WithSudoPreserveEnv ¶ added in v0.0.3
func WithSudoPreserveEnv() SudoOption
WithSudoPreserveEnv preserves the environment.
func WithSudoUser ¶ added in v0.0.3
func WithSudoUser(user string) SudoOption
WithSudoUser sets the target user.
type TargetOS ¶
type TargetOS int
TargetOS identifies the operating system of the target environment.
func DetectLocalOS ¶
func DetectLocalOS() TargetOS
DetectLocalOS returns the TargetOS of the current running process.
func ParseTargetOS ¶
ParseTargetOS converts a typical OS string (e.g., "linux", "darwin") to a TargetOS.
type TransportError ¶ added in v0.1.0
TransportError represents a failure in the underlying transport layer (e.g., connection lost, docker daemon unreachable, binary not found).
TransportErrors are retryable via WithRetry, unlike ExitErrors which represent a definitive command result.
func (*TransportError) Error ¶ added in v0.1.0
func (e *TransportError) Error() string
func (*TransportError) Unwrap ¶ added in v0.1.0
func (e *TransportError) Unwrap() error
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
fileutil
Package fileutil provides shared file-transfer utilities for invoke providers.
|
Package fileutil provides shared file-transfer utilities for invoke providers. |
|
Package invoketest provides a contract test suite for invoke providers.
|
Package invoketest provides a contract test suite for invoke providers. |
|
providers
|
|
|
local
module
|
|
|
mock
module
|
|
|
ssh
module
|