commander

package module
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Mar 22, 2026 License: MIT Imports: 16 Imported by: 1

README ยถ

  โ˜ฉ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• โ˜ฉ
     __________  __  _____  ______    _   ______  __________ 
    / ____/ __ \/  |/  /  |/  /   |  / | / / __ \/ ____/ __ \
   / /   / / / / /|_/ / /|_/ / /| | /  |/ / / / / __/ / /_/ /
  / /___/ /_/ / /  / / /  / / ___ |/ /|  / /_/ / /___/ _, _/ 
  \____/\____/_/  /_/_/  /_/_/  |_/_/ |_/_____/_____/_/ |_|
                             ๐Ÿš๐Ÿ’€๐Ÿ”ฅ
  โ˜ฉ โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• โ˜ฉ

Commander ๐Ÿš

SOMEBODY STOP ME! ๐Ÿ’š Command execution from hell's kitchen ๐Ÿ”ฅ๐Ÿ˜ˆ

Commander takes Go's os/exec and transforms it from a fucking disaster ๐Ÿ’ฅ into something that actually works - P-A-R-T-Y! ๐ŸŽ‰ This shit wraps all the garbage ๐Ÿ—‘๏ธ that makes you want to violate everything holy and spawn some digital violence ๐Ÿ”ช๐Ÿ’ป. No more hanging processes (they picked the wrong god to pray to! โšฐ๏ธ), no more race conditions giving you digital hemorrhoids ๐Ÿฉธ, no more timeout bullshit โฐ๐Ÿ’ฉ that makes you question why you didn't just become a tortured soul in the first place ๐Ÿ‘น.

SMOKIN'! ๐Ÿšฌ: Stream output like the green-faced monster ๐Ÿ‘น๐Ÿ’š you were born to be, terminate processes with malevolent glee ๐Ÿ’€๐Ÿ”ฅ, mock everything without losing your goddamn sanity ๐Ÿง ๐Ÿ’ฅ, and handle errors like a true hellspawn console warrior ๐Ÿ‘บ๐Ÿ’ป. I am returned from the darkness to tear your shitty command execution apart! โš”๏ธ๐Ÿ–ฅ๏ธ๐Ÿ”ฅ

Installation

go get github.com/psyb0t/commander

The Interfaces ๐Ÿ› ๏ธ (Know Your Weapons ๐Ÿ—ก๏ธโš”๏ธ)

Commander Interface ๐Ÿ’š๐ŸŽญ - The main event (SOMEBODY STOP ME! ๐Ÿ‘น)
type Commander interface {
    // Fire and forget - P-A-R-T-Y! ๐Ÿ”ฅ๐ŸŽ‰ Just run the damn thing ๐Ÿ’จ
    Run(ctx context.Context, name string, args []string, opts ...Option) error
    
    // Get stdout/stderr separated - it's showtime! ๐ŸŽญ๐Ÿ’ฅ
    Output(ctx context.Context, name string, args []string, opts ...Option) (stdout []byte, stderr []byte, err error)
    
    // Mix that output together - VIOLATE EVERYTHING HOLY! ๐Ÿ‘น๐Ÿ’€
    CombinedOutput(ctx context.Context, name string, args []string, opts ...Option) (output []byte, err error)
    
    // Start a process you can control - somebody stop me! ๐Ÿ›‘๐Ÿ‘น๐Ÿ”ฅ
    Start(ctx context.Context, name string, args []string, opts ...Option) (Process, error)
}
Process Interface ๐Ÿ”ฅ๐Ÿ‘น - Hellspawn fuckery from the abyss โšฐ๏ธ๐Ÿ’€
type Process interface {
    Start() error                                            // Spawn the beast - it's PARTY TIME! ๐ŸŽ‰๐Ÿ‘น๐Ÿ”ฅ
    Wait() error                                            // Wait for the carnage to finish โฐ๐Ÿ’€
    StdinPipe() (io.WriteCloser, error)                    // Feed the machine - VIOLATE EVERYTHING! ๐Ÿšฐ๐Ÿ”ช
    Stream(stdout, stderr chan<- string)                    // Stream the chaos live - witness the violence! ๐ŸŒ๐Ÿ’ป๐Ÿ“กโšก
    Stop(ctx context.Context) error                         // They picked the wrong god to pray to! โšฐ๏ธ๐Ÿ‘น๐Ÿ’€
    Kill(ctx context.Context) error                        // Somebody stop me from this beautiful murder! ๐Ÿ”ซ๐Ÿ’ฅ๐Ÿ’š
    PID() int                                               // Get the process ID - know thy enemy! ๐ŸŽฏ๐Ÿ‘น๐Ÿ”ข
}
Options โš™๏ธ๐Ÿ‘น - Pimp your malevolent machinery ๐Ÿ”ฅ๐Ÿ’š

Command Execution Options:

func WithStdin(stdin io.Reader) Option        // Feed the beast - it's party time! ๐Ÿฝ๏ธ๐Ÿ‘น๐ŸŽ‰
func WithEnv(env []string) Option            // Corrupt the environment - spawn the chaos! ๐ŸŒ๐Ÿ’ป๐Ÿ”ฅ๐Ÿ‘บ
func WithDir(dir string) Option              // Choose your battlefield - violate everything holy! ๐Ÿ“๐Ÿ—‚๏ธโšฐ๏ธ

Basic Usage ๐Ÿ’šโš”๏ธ - The fundamentals of digital violence ๐Ÿ”ฅ๐Ÿ’€

Simple Command Execution
package main

import (
    "context"
    "errors"
    "fmt"
    "log"

    "github.com/psyb0t/commander"
    commonerrors "github.com/psyb0t/common-go/errors"
)

func main() {
    cmd := commander.New()
    ctx := context.Background()

    // Just run dat shit and forget about it - wicked!
    err := cmd.Run(ctx, "echo", []string{"hello world"})
    if err != nil {
        log.Fatal("Failed to run command - what a fucking disaster:", err)
    }

    // Get da output like a civilized person, innit
    stdout, stderr, err := cmd.Output(ctx, "ls", []string{"-la", "/tmp"})
    if err != nil {
        log.Fatal("Command failed - dis is well fucked:", err)
    }
    
    fmt.Printf("Files:\n%s\n", stdout)
    if len(stderr) > 0 {
        fmt.Printf("Errors (oh for fuck's sake):\n%s\n", stderr)
    }

    // When you don't give a toss about separating streams
    output, err := cmd.CombinedOutput(ctx, "git", []string{"status"})
    if err != nil {
        log.Fatal("Git failed - typical fucking git:", err)
    }
    fmt.Printf("Git says (probably some bullshit):\n%s\n", output)
}

Advanced Shit - Real-time Streaming, bruv

Want to see what's happening while it's happening? Here's how you stream dat shit live - it's well good:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/psyb0t/commander"
)

func main() {
    cmd := commander.New()
    ctx := context.Background()

    // Start a long-running process
    proc, err := cmd.Start(ctx, "ping", []string{"-c", "10", "google.com"})
    if err != nil {
        log.Fatal("Failed to start ping:", err)
    }

    // Create channels for live streaming
    stdout := make(chan string, 100)  // Buffer it so we don't block
    stderr := make(chan string, 100)

    // Start streaming (this is non-blocking)
    proc.Stream(stdout, stderr)

    // Read the streams as they come in
    go func() {
        for line := range stdout {
            fmt.Printf("[PING] %s\n", line)
        }
    }()

    go func() {
        for line := range stderr {
            fmt.Printf("[ERROR] %s\n", line)
        }
    }()

    // Wait for the process to finish
    err = proc.Wait()
    if err != nil {
        fmt.Printf("Ping finished with error: %v\n", err)
    } else {
        fmt.Println("Ping completed successfully!")
    }
}

Process Control - Be the Boss

Graceful Termination
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "time"

    "github.com/psyb0t/commander"
    commonerrors "github.com/psyb0t/common-go/errors"
)

func main() {
    cmd := commander.New()
    ctx := context.Background()

    // Start something that runs forever
    proc, err := cmd.Start(ctx, "tail", []string{"-f", "/var/log/syslog"})
    if err != nil {
        log.Fatal("Failed to start tail:", err)
    }

    // Let it run for a bit
    time.Sleep(2 * time.Second)

    // Now shut it down gracefully (SIGTERM first, SIGKILL after 5 seconds if needed)
    fmt.Println("Shutting down gracefully...")
    stopCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    err = proc.Stop(stopCtx)
    
    if err == nil {
        fmt.Println("Process stopped cleanly")
    } else if errors.Is(err, commonerrors.ErrTerminated) {
        fmt.Println("Process terminated gracefully (SIGTERM)")
    } else if errors.Is(err, commonerrors.ErrKilled) {
        fmt.Println("Process had to be killed (SIGKILL)")
    } else {
        fmt.Printf("Stop failed: %v\n", err)
    }
}
Spank you! Spank you very much! (Immediate Process Termination)
func justKillIt() {
    cmd := commander.New()
    ctx := context.Background()

    proc, _ := cmd.Start(ctx, "sleep", []string{"1000"})
    
    // Like a glove! No mercy, just beautiful violence
    err := proc.Kill(ctx)
    if errors.Is(err, commonerrors.ErrKilled) {
        fmt.Println("Process killed with SIGKILL - somebody stop me!")
    } else if err != nil {
        fmt.Printf("Kill failed: %v\n", err)
    }
}

Custom Kill Signals ๐Ÿ’€โš”๏ธ - Choose your weapon of destruction

Using Custom Signals for Graceful Termination
package main

import (
    "context"
    "fmt"
    "syscall"
    "time"

    "github.com/psyb0t/commander"
)

func killWithStyle() {
    cmd := commander.New()
    ctx := context.Background()

    proc, err := cmd.Start(ctx, "your-daemon", []string{"--config", "prod.yml"})
    if err != nil {
        panic(err)
    }

    // Give it 10 seconds to shut down gracefully with SIGINT instead of SIGTERM
    stopCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    
    err = proc.Stop(stopCtx)
    if err != nil {
        fmt.Printf("Process stopped with: %v\n", err)
    }
}

func useUserSignals() {
    cmd := commander.New()
    ctx := context.Background()

    proc, err := cmd.Start(ctx, "nginx", []string{"-g", "daemon off;"})
    if err != nil {
        panic(err)
    }

    // Nginx responds to SIGUSR1 for graceful reload
    stopCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()
    
    err = proc.Stop(stopCtx)
    if err != nil {
        fmt.Printf("Nginx graceful reload result: %v\n", err)
    }
}

Timeout Handling - Patience is for suckers

Context-Based Timeouts (The Right Wayโ„ข๏ธ)
func contextTimeout() {
    cmd := commander.New()
    
    // This will timeout after 2 seconds - context controls everything!
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    err := cmd.Run(ctx, "sleep", []string{"10"})
    if errors.Is(err, commonerrors.ErrTimeout) {
        fmt.Println("Bingo! Command timed out like a champ!")
    }
}

// Stop with custom timeout - no more redundant bullshit!
func stopWithTimeout() {
    cmd := commander.New()
    ctx := context.Background()
    
    proc, _ := cmd.Start(ctx, "sleep", []string{"100"})
    
    // Give it 3 seconds to die gracefully, then SIGKILL the fucker
    stopCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()
    
    err := proc.Stop(stopCtx) // Clean as fuck - context controls timeout!
    if errors.Is(err, commonerrors.ErrTerminated) {
        fmt.Println("Process gracefully terminated!")
    } else if errors.Is(err, commonerrors.ErrKilled) {
        fmt.Println("Process was force killed after timeout!")
    }
}

API Migration Guide ๐Ÿ”„ - From the old shit to the new hotness

Old API (Redundant bullshit):

// OLD - Don't use this crap anymore!
err := proc.Stop(ctx, 5*time.Second) // WTF? Both ctx AND timeout?

New API (Clean as fuck):

// NEW - Context controls everything like a boss!
stopCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
err := proc.Stop(stopCtx) // One source of truth for timeouts

// Graceful stop with SIGTERM then SIGKILL
err := proc.Stop(stopCtx)

// No timeout? No problem - immediate force kill
err := proc.Stop(context.Background()) // No deadline = force kill

Why the change? Because having both ctx and timeout parameters was fucking redundant! Now the user has full control - use context.WithTimeout(), context.WithDeadline(), context.WithCancel(), or any other context pattern. Much cleaner and follows Go idioms properly.

Input and Environment - Feeding time at the process zoo

Stdin Input
func stdinExample() {
    cmd := commander.New()
    ctx := context.Background()

    // Feed some data to wc to count lines
    input := strings.NewReader("line 1\nline 2\nline 3\nline 4\n")
    
    stdout, _, err := cmd.Output(ctx, "wc", []string{"-l"}, 
        commander.WithStdin(input))
    
    if err != nil {
        log.Fatal("wc failed:", err)
    }

    fmt.Printf("Line count: %s", stdout) // Should print "4" - Smokin'!
}
Environment Variables
func environmentExample() {
    cmd := commander.New()
    ctx := context.Background()

    // Set custom environment
    stdout, _, err := cmd.Output(ctx, "sh", []string{"-c", "echo $CUSTOM_VAR $ANOTHER_VAR"}, 
        commander.WithEnv([]string{
            "CUSTOM_VAR=hello",
            "ANOTHER_VAR=world",
        }))
    
    if err != nil {
        log.Fatal("Shell command failed:", err)
    }

    fmt.Printf("Environment output: %s", stdout) // Should print "hello world" - Alllllrighty then!
}
Working Directory
func workingDirectoryExample() {
    cmd := commander.New()
    ctx := context.Background()

    // Run pwd in /tmp
    stdout, _, err := cmd.Output(ctx, "pwd", nil, 
        commander.WithDir("/tmp"))
    
    if err != nil {
        log.Fatal("pwd failed:", err)
    }

    fmt.Printf("Current directory: %s", stdout) // Should print "/tmp"
}
All Options Combined
func kitchenSinkExample() {
    cmd := commander.New()
    ctx := context.Background()

    input := strings.NewReader("some input data")
    
    // Use context timeout instead of WithTimeout option
    timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    stdout, stderr, err := cmd.Output(timeoutCtx, "cat", nil,
        commander.WithStdin(input),
        commander.WithDir("/tmp"),
        commander.WithEnv([]string{"LANG=en_US.UTF-8"}))
    
    if err != nil {
        log.Fatal("Kitchen sink failed:", err)
    }

    fmt.Printf("Output: %s\n", stdout)
    if len(stderr) > 0 {
        fmt.Printf("Errors: %s\n", stderr)
    }
}

Advanced Streaming - Multiple Listeners

You can have multiple channels listening to the same process output:

func multipleListeners() {
    cmd := commander.New()
    ctx := context.Background()

    proc, err := cmd.Start(ctx, "ping", []string{"-c", "5", "google.com"})
    if err != nil {
        log.Fatal("Failed to start ping:", err)
    }

    // Create multiple listeners
    logger := make(chan string, 100)
    display := make(chan string, 100)
    storage := make(chan string, 100)

    // All three will get the same data
    proc.Stream(logger, nil)
    proc.Stream(display, nil) 
    proc.Stream(storage, nil)

    // Handle each stream differently
    go func() {
        for line := range logger {
            log.Printf("[LOG] %s", line)
        }
    }()

    go func() {
        for line := range display {
            fmt.Printf("[DISPLAY] %s\n", line)
        }
    }()

    var stored []string
    go func() {
        for line := range storage {
            stored = append(stored, line)
        }
        fmt.Printf("Stored %d lines total\n", len(stored))
    }()

    err = proc.Wait()
    if err != nil {
        fmt.Printf("Process failed: %v\n", err)
    }
}

Concurrent Execution - Go Wild

Run multiple commands at the same time like a fucking machine:

func concurrentExecution() {
    cmd := commander.New()
    ctx := context.Background()

    // Commands to run concurrently
    commands := []struct {
        name string
        args []string
    }{
        {"echo", []string{"first"}},
        {"echo", []string{"second"}},
        {"echo", []string{"third"}},
        {"sleep", []string{"1"}},
        {"date", nil},
    }

    var wg sync.WaitGroup
    results := make(chan string, len(commands))

    // Launch all commands concurrently
    for _, cmdInfo := range commands {
        wg.Add(1)
        go func(name string, args []string) {
            defer wg.Done()
            
            stdout, _, err := cmd.Output(ctx, name, args)
            if err != nil {
                results <- fmt.Sprintf("ERROR: %s %v failed: %v", name, args, err)
                return
            }
            
            results <- fmt.Sprintf("SUCCESS: %s %v -> %s", name, args, strings.TrimSpace(string(stdout)))
        }(cmdInfo.name, cmdInfo.args)
    }

    // Wait for all to complete
    wg.Wait()
    close(results)

    // Show results
    fmt.Println("Concurrent execution results:")
    for result := range results {
        fmt.Printf("  %s\n", result)
    }
}

Error Handling - Know What Went Wrong

The package gives you specific error types so you know exactly what happened:

func errorHandling() {
    cmd := commander.New()
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    proc, err := cmd.Start(ctx, "sleep", []string{"10"})
    if err != nil {
        log.Fatal("Failed to start process:", err)
    }

    err = proc.Wait()
    if err == nil {
        fmt.Println("โœ… Process completed successfully")
    } else if errors.Is(err, commonerrors.ErrTimeout) {
        fmt.Println("โŒ Process timed out")
    } else if errors.Is(err, commonerrors.ErrTerminated) {
        fmt.Println("โš ๏ธ  Process was terminated (SIGTERM)")
    } else if errors.Is(err, commonerrors.ErrKilled) {
        fmt.Println("๐Ÿ’€ Process was killed (SIGKILL)")
    } else {
        fmt.Printf("๐Ÿ’ฅ Process failed: %v\n", err)
    }
}

Testing - Mock That Shit

The package comes with a comprehensive mocking system so you can test without actually running commands:

Basic Mocking
func TestMyFunction(t *testing.T) {
    mock := commander.NewMock()
    defer func() {
        if err := mock.VerifyExpectations(); err != nil {
            t.Error("Mock expectations failed:", err)
        }
    }()

    // Set up expectations
    mock.Expect("git", "status").ReturnOutput([]byte("On branch main\nnothing to commit, working tree clean"))
    mock.Expect("git", "push").ReturnError(errors.New("push failed"))

    // Use the mock in your code (it implements Commander interface)
    err := myDeployFunction(mock)
    
    // Your function should handle the push failure gracefully
    assert.Error(t, err)
}
Advanced Argument Matching
func TestWithMatchers(t *testing.T) {
    mock := commander.NewMock()
    defer mock.VerifyExpectations()

    // Exact matching (default)
    mock.Expect("echo", "hello").ReturnOutput([]byte("hello"))

    // Regex matching
    mock.ExpectWithMatchers("grep", 
        commander.Regex("^error.*"),  // First arg must match regex
        commander.Exact("logfile.txt"))  // Second arg must be exact

    // Wildcard matching
    mock.ExpectWithMatchers("find", commander.Any(), commander.Any())

    // Mixed matching
    mock.ExpectWithMatchers("rsync",
        commander.Exact("-av"),
        commander.Regex(`.*\.tar\.gz$`),
        commander.Any())

    // Test your code here...
}
Process Mocking
func TestProcessMocking(t *testing.T) {
    mock := commander.NewMock()

    // Mock a streaming process
    mock.Expect("tail", "-f", "/var/log/messages").
        ReturnOutput([]byte("log line 1\nlog line 2\nlog line 3"))

    proc, err := mock.Start(context.Background(), "tail", []string{"-f", "/var/log/messages"})
    require.NoError(t, err)

    // Test streaming
    stdout := make(chan string, 10)
    proc.Stream(stdout, nil)

    var lines []string
    for line := range stdout {
        lines = append(lines, line)
    }

    expected := []string{"log line 1", "log line 2", "log line 3"}
    assert.Equal(t, expected, lines)

    require.NoError(t, mock.VerifyExpectations())
}
Mock Utilities
func TestMockUtilities(t *testing.T) {
    mock := commander.NewMock()

    // Set up multiple expectations
    mock.Expect("first").ReturnOutput([]byte("1"))
    mock.Expect("second").ReturnOutput([]byte("2"))
    mock.Expect("third").ReturnError(errors.New("failed"))

    // Execute them
    mock.Output(context.Background(), "first", nil)
    mock.Output(context.Background(), "second", nil)
    mock.Output(context.Background(), "third", nil)

    // Check call order
    order := mock.CallOrder()
    expected := []string{"first ", "second ", "third "}
    assert.Equal(t, expected, order)

    // Reset if needed
    mock.Reset() // Clears all expectations and history

    require.NoError(t, mock.VerifyExpectations())
}

Performance and Concurrency

Thread Safety

Everything is thread-safe. You can:

  • Use the same Commander instance from multiple goroutines
  • Run multiple commands concurrently
  • Stream from multiple processes simultaneously
  • Use mocks in parallel tests
func TestConcurrentMocking(t *testing.T) {
    mock := commander.NewMock()

    // Set up expectations for concurrent calls
    for i := 0; i < 10; i++ {
        mock.Expect("echo", string(rune('a'+i))).
            ReturnOutput([]byte(string(rune('A'+i))))
    }

    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(index int) {
            defer wg.Done()
            
            stdout, _, err := mock.Output(context.Background(), 
                "echo", []string{string(rune('a'+index))})
            
            require.NoError(t, err)
            assert.Equal(t, string(rune('A'+index)), string(stdout))
        }(i)
    }
    
    wg.Wait()
    require.NoError(t, mock.VerifyExpectations())
}
Memory Management
  • Channels are automatically closed when processes end
  • Context cancellation is properly handled
  • No memory leaks from goroutines or file descriptors
  • Process cleanup uses sync.Once for safety

Error Types Reference

Here's all the shit that can go wrong and how to handle it:

// Package-specific errors
var (
    ErrUnexpectedCommand        = errors.New("unexpected command")
    ErrExpectedCommandNotCalled = errors.New("expected command not called")
    ErrProcessStartFailed       = errors.New("process start failed")
    ErrProcessWaitFailed        = errors.New("process wait failed")
    ErrPipeCreationFailed       = errors.New("pipe creation failed")
    ErrCommandFailed            = errors.New("command failed")
)

// Common errors (from github.com/psyb0t/common-go/errors)
commonerrors.ErrTimeout     // Command timed out
commonerrors.ErrTerminated  // Process terminated by SIGTERM
commonerrors.ErrKilled      // Process killed by SIGKILL

Real-world Examples

Deploy Script
func deployApp(cmd commander.Commander) error {
    ctx := context.Background()

    fmt.Println("๐Ÿ—๏ธ  Building application...")
    err := cmd.Run(ctx, "go", []string{"build", "-o", "app", "./cmd/server"})
    if err != nil {
        return fmt.Errorf("build failed: %w", err)
    }

    fmt.Println("๐Ÿงช Running tests...")
    testCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
    defer cancel()
    err = cmd.Run(testCtx, "go", []string{"test", "./..."})
    if err != nil {
        return fmt.Errorf("tests failed: %w", err)
    }

    fmt.Println("๐Ÿ“ฆ Creating Docker image...")
    err = cmd.Run(ctx, "docker", []string{"build", "-t", "myapp:latest", "."})
    if err != nil {
        return fmt.Errorf("docker build failed: %w", err)
    }

    fmt.Println("๐Ÿš€ Pushing to registry...")
    pushCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
    defer cancel()
    err = cmd.Run(pushCtx, "docker", []string{"push", "myapp:latest"})
    if err != nil {
        return fmt.Errorf("docker push failed: %w", err)
    }

    fmt.Println("โœ… Deploy completed successfully!")
    return nil
}
Log Monitor
func monitorLogs(cmd commander.Commander) error {
    ctx := context.Background()

    proc, err := cmd.Start(ctx, "tail", []string{"-f", "/var/log/app.log"})
    if err != nil {
        return fmt.Errorf("failed to start log monitoring: %w", err)
    }

    stdout := make(chan string, 100)
    proc.Stream(stdout, nil)

    // Monitor for specific patterns
    errorPattern := regexp.MustCompile(`(?i)error|exception|panic`)
    warningPattern := regexp.MustCompile(`(?i)warning|warn`)

    go func() {
        for line := range stdout {
            switch {
            case errorPattern.MatchString(line):
                log.Printf("๐Ÿšจ ERROR: %s", line)
                // Maybe send alert, page someone, etc.
            case warningPattern.MatchString(line):
                log.Printf("โš ๏ธ  WARNING: %s", line)
            default:
                log.Printf("โ„น๏ธ  INFO: %s", line)
            }
        }
    }()

    // Stop monitoring after 1 hour
    time.Sleep(1 * time.Hour)
    
    stopCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    return proc.Stop(stopCtx)
}
System Health Check
func healthCheck(cmd commander.Commander) (map[string]bool, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    checks := map[string][]string{
        "disk_space":    {"df", "-h", "/"},
        "memory":        {"free", "-m"},
        "load_average":  {"uptime"},
        "docker":        {"docker", "ps"},
        "nginx":         {"systemctl", "is-active", "nginx"},
        "database":      {"pg_isready"},
    }

    results := make(map[string]bool)
    var wg sync.WaitGroup

    for name, cmdArgs := range checks {
        wg.Add(1)
        go func(checkName string, args []string) {
            defer wg.Done()
            
            err := cmd.Run(ctx, args[0], args[1:])
            results[checkName] = (err == nil)
            
            if err != nil {
                log.Printf("โŒ Health check '%s' failed: %v", checkName, err)
            } else {
                log.Printf("โœ… Health check '%s' passed", checkName)
            }
        }(name, cmdArgs)
    }

    wg.Wait()
    return results, nil
}

Dependencies

  • github.com/sirupsen/logrus - For debug logging
  • github.com/psyb0t/ctxerrors - Error wrapping with context
  • github.com/psyb0t/common-go - Common error types
  • Standard library: context, os/exec, sync, syscall, etc.

Why Use This?

Before (stdlib os/exec)
// Painful, error-prone, lots of boilerplate
cmd := exec.CommandContext(ctx, "some-command", "arg1", "arg2")
stdout, err := cmd.StdoutPipe()
if err != nil {
    // handle error
}
stderr, err := cmd.StderrPipe()
if err != nil {
    // handle error
}

err = cmd.Start()
if err != nil {
    // handle error
}

// Now you need to read from pipes in goroutines...
// And handle timeouts manually...
// And figure out why your process is hanging...
// And write your own mocks...
// ๐Ÿคฎ
After (Commander)
// Clean, simple, powerful
cmd := commander.New()
stdout, stderr, err := cmd.Output(ctx, "some-command", []string{"arg1", "arg2"})
if err != nil {
    // handle error (with proper context!)
}
// Done. That's it. ๐ŸŽ‰

License

MIT - Use it, abuse it, whatever. Just don't blame anyone if your servers catch fire. ๐Ÿ”ฅ

Contributing

Found a bug? Want a feature? Open an issue or send a PR. Contributing is welcome.


Commander: SOMEBODY STOP ME from this beautiful command-line carnage! P-A-R-T-Y time for your digital violence! ๐Ÿ”ฅ๐Ÿ‘น๐Ÿ’šโšฐ๏ธ ๐Ÿš

Documentation ยถ

Index ยถ

Constants ยถ

This section is empty.

Variables ยถ

View Source
var (
	ErrUnexpectedCommand        = errors.New("unexpected command")
	ErrExpectedCommandNotCalled = errors.New("expected command not called")
)

Execution errors

View Source
var (
	ErrProcessStartFailed = errors.New("process start failed")
	ErrProcessWaitFailed  = errors.New("process wait failed")
	ErrPipeCreationFailed = errors.New("pipe creation failed")
)

Process errors

View Source
var (
	ErrCommandFailed = errors.New("command failed")
)

Command execution errors

Functions ยถ

This section is empty.

Types ยถ

type AnyMatcher ยถ

type AnyMatcher struct{}

Any matcher

func (*AnyMatcher) Matches ยถ

func (m *AnyMatcher) Matches(_ string) bool

func (*AnyMatcher) String ยถ

func (m *AnyMatcher) String() string

type ArgumentMatcher ยถ

type ArgumentMatcher interface {
	Matches(arg string) bool
	String() string
}

ArgumentMatcher interface for flexible argument matching

func Any ยถ

func Any() ArgumentMatcher

func Exact ยถ

func Exact(s string) ArgumentMatcher

Helper functions for creating matchers

func Regex ยถ

func Regex(pattern string) ArgumentMatcher

type Commander ยถ

type Commander interface {
	// Run executes a command and waits for completion
	Run(
		ctx context.Context,
		name string,
		args []string,
		opts ...Option,
	) error

	// Output executes a command and returns stdout, stderr, and error
	Output(
		ctx context.Context,
		name string,
		args []string,
		opts ...Option,
	) (stdout []byte, stderr []byte, err error)

	// CombinedOutput executes a command and returns combined stdout+stderr and error
	CombinedOutput(
		ctx context.Context,
		name string,
		args []string,
		opts ...Option,
	) (output []byte, err error)

	// Start creates a command that can be controlled manually
	Start(
		ctx context.Context,
		name string,
		args []string,
		opts ...Option,
	) (Process, error)
}

func New ยถ

func New() Commander

type ExactMatcher ยถ

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

Exact matcher

func (*ExactMatcher) Matches ยถ

func (m *ExactMatcher) Matches(arg string) bool

func (*ExactMatcher) String ยถ

func (m *ExactMatcher) String() string

type Expectation ยถ

type Expectation struct {
	Name     string
	Args     []string
	Matchers []ArgumentMatcher
	Output   []byte
	Error    error
	Called   bool
}

func (*Expectation) ReturnError ยถ

func (e *Expectation) ReturnError(err error) *Expectation

func (*Expectation) ReturnOutput ยถ

func (e *Expectation) ReturnOutput(output []byte) *Expectation

Expectation methods

type MockCommander ยถ

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

MockCommander for testing

func NewMock ยถ

func NewMock() *MockCommander

func (*MockCommander) CallOrder ยถ

func (m *MockCommander) CallOrder() []string

func (*MockCommander) CombinedOutput ยถ

func (m *MockCommander) CombinedOutput(
	_ context.Context,
	name string,
	args []string,
	_ ...Option,
) ([]byte, error)

func (*MockCommander) Expect ยถ

func (m *MockCommander) Expect(
	name string,
	args ...string,
) *Expectation

func (*MockCommander) ExpectWithMatchers ยถ

func (m *MockCommander) ExpectWithMatchers(
	name string,
	matchers ...ArgumentMatcher,
) *Expectation

func (*MockCommander) Output ยถ

func (m *MockCommander) Output(
	_ context.Context,
	name string,
	args []string,
	_ ...Option,
) ([]byte, []byte, error)

func (*MockCommander) Reset ยถ

func (m *MockCommander) Reset()

func (*MockCommander) Run ยถ

func (m *MockCommander) Run(
	_ context.Context,
	name string,
	args []string,
	_ ...Option,
) error

func (*MockCommander) Start ยถ

func (m *MockCommander) Start(
	_ context.Context,
	name string,
	args []string,
	_ ...Option,
) (Process, error)

func (*MockCommander) VerifyExpectations ยถ

func (m *MockCommander) VerifyExpectations() error

type Option ยถ

type Option func(*Options)

func WithDir ยถ

func WithDir(dir string) Option

func WithEnv ยถ

func WithEnv(env []string) Option

func WithStdin ยถ

func WithStdin(stdin io.Reader) Option

type Options ยถ

type Options struct {
	Stdin io.Reader
	Env   []string
	Dir   string
}

type Process ยถ

type Process interface {
	Start() error
	Wait() error
	StdinPipe() (io.WriteCloser, error)
	// Starts streaming from the current moment, not from the beginning
	// Multiple streams can be active simultaneously (broadcast)
	// Pass nil for channels you don't want to listen to
	Stream(stdout, stderr chan<- string)
	Stop(ctx context.Context) error
	Kill(ctx context.Context) error
	PID() int
}

type RegexMatcher ยถ

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

Regex matcher

func (*RegexMatcher) Matches ยถ

func (m *RegexMatcher) Matches(arg string) bool

func (*RegexMatcher) String ยถ

func (m *RegexMatcher) String() string

Jump to

Keyboard shortcuts

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