invoke

package module
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Feb 6, 2026 License: MIT Imports: 11 Imported by: 3

README

invoke

invoke is a Go library for unified command execution. It lets you run commands across Local, SSH, and Docker environments using a single interface.

It solves the "three codepaths" problem: instead of writing separate logic for os/exec, crypto/ssh, and the Docker SDK, you write your logic once and invoke handles the transport.

Why?

Running commands properly is annoying.

  • Local: os/exec is fine, but boilerplate heavy.
  • SSH: crypto/ssh is low-level; you're manually managing sessions and streams.
  • Docker: The SDK is verbose and requires stream demultiplexing.

invoke abstracts this all away.

Installation

go get github.com/ruffel/invoke

Quick Start

1. The Basics

Run a command locally, or on a remote server, just by swapping the provider.

ctx := context.Background()

// env := local.New()
env, _ := ssh.New(ssh.NewConfig("10.0.0.1", "root"))
defer env.Close()

// The Executor wrapper gives you nice helpers (buffering, sudo, etc)
exec := invoke.NewExecutor(env)

// Runs 'uptime' on the remote host
out, _ := exec.RunBuffered(ctx, &invoke.Command{Cmd: "uptime"})
fmt.Println(string(out.Stdout))
2. Streaming Output (The "Real World" Use Case)

For long-running jobs (builds, deploys), you don't want to buffer everything. invoke is streaming-first.

cmd := invoke.Command{
    Cmd: "docker",
    Args: []string{"build", "."},
    Stdout: os.Stdout, // Stream directly to your terminal
    Stderr: os.Stderr,
}

err := env.Run(ctx, cmd)
3. Sudo without the headache

We handle the sudo -n flags and prompt avoidance for you.

// Automatically wraps as: sudo -n -- apt-get update
exec.Run(ctx, cmd, invoke.WithSudo())
4. File Transfer

Uploads and downloads work recursively and consistently across all providers.

// Upload a local directory to a remote server
err := env.Upload(ctx, "./configs/nginx", "/etc/nginx", invoke.WithPermissions(0644))
5. Convenience (One-Liners)

For simple local scripts, you don't need to manually manage the environment.

// Run a shell one-liner
res, _ := local.RunShell(ctx, "ls -la | grep foo")

// Run a configured command
res, _ := local.RunCommand(ctx, &invoke.Command{Cmd: "ls", Dir: "/tmp"})
6. Fluent Builder

Construct commands without struct literals.

cmd := invoke.Cmd("docker").
    Arg("run").
    Arg("-it").
    Env("GOOS", "linux").
    Dir("/app").
    Build()

exec.Run(ctx, cmd)

Design Philosophy

  • Streaming First: We avoid buffering whenever possible. Interfaces use io.Reader and io.Writer.
  • Context Aware: All blocking operations accept context.Context. Cancellation works as you expect (sending signals to remote processes).
  • Secure Defaults: SSH host key checking is enforced by default. You have to opt-out explicitly.

Troubleshooting

"configuration error: HostKeyCheck is missing"

When using the SSH provider, you must verify the server's identity to prevent Man-in-the-Middle attacks.

  • Production: Provide a HostKeyCallback (e.g., parse your ~/.ssh/known_hosts file).
  • Testing: Use c.InsecureSkipVerify = true to disable check (NOT recommended for production).
"failed to dial ssh: ... handshake failed"
  • Check that your SSH key has the correct permissions (chmod 600).
  • Ensure the remote user exists and allows SSH login.
  • Verify AllowUsers or PermitRootLogin in the server's sshd_config.

License

MIT

Documentation

Overview

Package invoke provides a unified interface for command execution and file transfer.

Core Interfaces

- Environment: The connection to a system (Local, SSH, Docker). - Process: A running command handle (allows Wait, Signal, Close).

Streaming

`invoke` is streaming-first. We don't buffer output by default. If you want to capture stdout/stderr, attach an `io.Writer` to your `Command`.

For simple "just give me the output" cases, use the `Executor` wrapper.

Sudo

Privilege escalation is supported via `invoke.WithSudo()`. This 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

Examples

Constants

This section is empty.

Variables

View Source
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
	Stderr []byte
}

BufferedResult extends Result to include captured stdout/stderr content. Returned by Executor.RunBuffered.

type Builder added in v0.0.3

type Builder struct {
	// contains filtered or unexported fields
}

Builder provides a fluent API for constructing Commands.

func Cmd added in v0.0.3

func Cmd(binary string) *Builder

Cmd creates a new Builder for a command with the given name/path.

Example (Builder)
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/ruffel/invoke"
	"github.com/ruffel/invoke/providers/local"
)

func main() {
	// Construct a command using the fluent builder API.
	cmd := invoke.Cmd("go").
		Arg("env").
		Arg("GREETING").
		Env("GREETING", "hello builder").
		Dir(os.TempDir()).
		Build()

	// Execute it
	env, err := local.New()
	if err != nil {
		panic(err)
	}

	defer func() { _ = env.Close() }()

	ctx := context.Background()

	res, err := env.Run(ctx, cmd)
	if err != nil {
		panic(err)
	}

	// Capture output via RunBuffered would be better for verification,
	// but here we just check exit code to keep it simple.
	fmt.Printf("Exit Code: %d\n", res.ExitCode)

}
Output:
Exit Code: 0

func (*Builder) Arg added in v0.0.3

func (b *Builder) Arg(arg string) *Builder

Arg adds a single argument.

func (*Builder) Args added in v0.0.3

func (b *Builder) Args(args ...string) *Builder

Args adds multiple arguments.

func (*Builder) Build added in v0.0.3

func (b *Builder) Build() *Command

Build returns a deep copy of the constructed Command. The returned command is safe to use while the builder continues to be modified.

func (*Builder) Dir added in v0.0.3

func (b *Builder) Dir(dir string) *Builder

Dir sets the working directory.

func (*Builder) Env added in v0.0.3

func (b *Builder) Env(key, value string) *Builder

Env adds an environment variable in "KEY=VALUE" format.

func (*Builder) Input added in v0.0.3

func (b *Builder) Input(s string) *Builder

Input sets the standard input from a string.

func (*Builder) Stderr added in v0.0.3

func (b *Builder) Stderr(w io.Writer) *Builder

Stderr sets the standard error stream.

func (*Builder) Stdin added in v0.0.3

func (b *Builder) Stdin(r io.Reader) *Builder

Stdin sets the standard input stream.

func (*Builder) Stdout added in v0.0.3

func (b *Builder) Stdout(w io.Writer) *Builder

Stdout sets the standard output stream.

func (*Builder) Tty added in v0.0.3

func (b *Builder) Tty() *Builder

Tty enables PTY allocation.

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

func NewCommand(binary string, args ...string) *Command

NewCommand creates a new Command with the given binary and arguments.

func ParseCommand

func ParseCommand(cmdStr string) (*Command, error)

ParseCommand parses a shell command string into a Command struct using shlex. It handles quoted arguments correctly.

func (*Command) String

func (c *Command) String() string

String returns a simplified, shell-quoted string representation of the command.

type Environment

type Environment interface {
	io.Closer

	// Run executes a command synchronously.
	// Returns the result (exit code, error). Output is not captured by default; use 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().
	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.
	Upload(ctx context.Context, localPath, remotePath string, opts ...FileOption) error

	// Download copies a remote file or directory to the local destination.
	Download(ctx context.Context, remotePath, localPath string, opts ...FileOption) error

	// LookPath searches for an executable named file in the directories named by
	// the PATH environment variable.
	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)
	}
}
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.

func WithRetry

func WithRetry(attempts int, delay time.Duration) ExecOption

WithRetry enables retry logic for the command execution using linear backoff. attempts: Total number of attempts (including the initial one). Must be >= 1. delay: Duration to wait between attempts.

func WithSudo

func WithSudo(opts ...SudoOption) ExecOption

WithSudo wraps the command in sudo.

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

func (e *Executor) LookPath(ctx context.Context, file string) (string, error)

LookPath resolves an executable path using the underlying environment's LookPath strategy.

func (*Executor) Run

func (e *Executor) Run(ctx context.Context, cmd *Command, opts ...ExecOption) (*Result, error)

Run executes a command, respecting context cancellation and configured retry policies.

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.

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) RunLineStream

func (e *Executor) RunLineStream(ctx context.Context, cmd *Command, onLine func(string), _ ...ExecOption) error

RunLineStream streams stdout line-by-line to onLine. Useful for live logging. Overrides Command.Stdout.

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

func (e *Executor) Start(ctx context.Context, cmd *Command) (Process, error)

Start initiates a command asynchronously. Caller is responsible for Process.Wait().

func (*Executor) TargetOS

func (e *Executor) TargetOS() TargetOS

TargetOS returns the operating system of the underlying environment.

func (*Executor) Upload

func (e *Executor) Upload(ctx context.Context, localPath, remotePath string, opts ...FileOption) error

Upload copies a local file or directory to the remote destination. It delegates directly to the underlying Environment.

type ExitError

type ExitError struct {
	Command  *Command
	ExitCode int
	Stderr   []byte // Captured stderr, if available from RunBuffered
	Cause    error  // Underlying error, if any
}

ExitError represents a successful execution that resulted in a non-zero exit code.

func (*ExitError) Error

func (e *ExitError) Error() string

func (*ExitError) Unwrap

func (e *ExitError) Unwrap() error

type FileConfig

type FileConfig struct {
	Permissions os.FileMode // Destination perms override (0 means preserve/default)
	UID, GID    int         // Destination ownership (0 usually means root/current)
	Recursive   bool        // Default true for generic uploads
	Progress    ProgressFunc
}

FileConfig holds configuration for file transfers.

func DefaultFileConfig

func DefaultFileConfig() FileConfig

DefaultFileConfig returns defaults.

type FileOption

type FileOption func(*FileConfig)

FileOption defines a functional option for file transfers.

func WithOwner

func WithOwner(uid, gid int) FileOption

WithOwner forces specific destination ownership.

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.

type Process

type Process interface {
	io.Closer

	// Wait blocks until the process exits.
	// Returns an error if the exit code is non-zero.
	Wait() error

	// Result returns metadata (exit code, termination status) (only valid after Wait).
	Result() *Result

	// Signal sends an OS signal to the process.
	// Note: 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 // Time taken for execution
	Error    error         // Launch/Transport error (distinct from non-zero exit code)
}

Result contains metadata about a completed command execution.

func (*Result) Failed

func (r *Result) Failed() bool

Failed returns true if the command failed (non-zero exit code or transport error).

func (*Result) Success

func (r *Result) Success() bool

Success returns true if the command completed with exit code 0 and no transport 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 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.

const (
	// OSUnknown represents an unidentified operating system.
	OSUnknown TargetOS = iota
	// OSLinux represents the Linux kernel.
	OSLinux
	// OSWindows represents Microsoft Windows.
	OSWindows
	// OSDarwin represents macOS (Darwin).
	OSDarwin
)

func DetectLocalOS

func DetectLocalOS() TargetOS

DetectLocalOS returns the TargetOS of the current running process.

func ParseTargetOS

func ParseTargetOS(osStr string) TargetOS

ParseTargetOS converts a typical OS string (e.g., "linux", "darwin") to a TargetOS.

func (TargetOS) ShellCommand

func (os TargetOS) ShellCommand(script string) *Command

ShellCommand constructs a command that runs the provided script inside the system shell. Returns "sh -c <script>" for UNIX-likes and "powershell ..." for Windows.

func (TargetOS) String

func (os TargetOS) String() string

Directories

Path Synopsis
providers
local module
mock module
ssh module

Jump to

Keyboard shortcuts

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