Commander: Bcuz Go’s os/exec Made Me Want to Shart on My Laptop’s Screen

Let’s be fucking honest here – if you’ve ever tried to do anything serious with Go’s os/exec package, you know it’s about as user-friendly as a cactus suppository. Want to stream output in real-time? Good luck with that bullshit. Need to handle timeouts properly? Hope you enjoy writing the same boilerplate garbage over and over. Want to test your command execution? Get ready to mock the entire fucking universe.

After years of dealing with this nonsense, I finally said “fuck it” and built something that actually works. Meet Commander – a Go package that wraps os/exec and makes it not completely terrible.

The Problem: os/exec Wants to Make You Suffer

Here’s what using the standard library looks like when you want to do something as simple as running a command with a timeout:

// The "standard" way - prepare for pain
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "some-long-running-thing", "arg1", "arg2")
stdout, err := cmd.StdoutPipe()
if err != nil {
    // handle error like a good little gopher
}
stderr, err := cmd.StderrPipe()
if err != nil {
    // more error handling bullshit
}
if err := cmd.Start(); err != nil {
    // jesus christ, more errors
}
// Now you get to read from pipes in goroutines like it's 1995
// Don't forget to handle all the edge cases!
// What if the context times out? What if the process hangs?
// What if you want to kill it gracefully? FIGURE IT OUT YOURSELF!

This is the kind of code that makes junior developers quit programming and become yoga instructors.

The Solution: Commander – Command Execution That Doesn’t Suck

Commander takes all this garbage and turns it into something a human being can actually use:

import (
    "github.com/psyb0t/commander"
    commonerrors "github.com/psyb0t/common-go/errors"
)
cmd := commander.New()
ctx := context.Background()
// Just run something and get the output - revolutionary concept
stdout, stderr, err := cmd.Output(ctx, "some-long-running-thing", []string{"arg1", "arg2"},
    commander.WithTimeout(30*time.Second))
if errors.Is(err, commonerrors.ErrTimeout) {
    log.Println("Command timed out, as expected")
} else if err != nil {
    log.Printf("Command failed: %v", err)
} else {
    log.Printf("Success! Output: %s", stdout)
}

That’s it. No pipe juggling, no goroutine circus, no “did I remember to close that thing?” anxiety. Just working code that does what you fucking expect.

Real-time Streaming Without the Headache

But wait, there’s more! Want to stream output live? The standard library makes you feel like you’re performing surgery with oven mitts. Commander makes it trivial:

proc, err := cmd.Start(ctx, "ping", []string{"-c", "10", "google.com"})
if err != nil {
    log.Fatal("Failed to start ping:", err)
}
// Stream that shit live
stdout := make(chan string, 100)
proc.Stream(stdout, nil)
// Watch the magic happen
go func() {
    for line := range stdout {
        fmt.Printf("[LIVE] %s\n", line)
    }
}()
err = proc.Wait()
if err != nil {
    log.Printf("Ping finished with: %v", err)
}

You can even have multiple listeners on the same process – something that would require a PhD in Go internals to implement with the standard library.

Process Control That Actually Makes Sense

Need to kill a process gracefully? The standard library basically tells you to go fuck yourself and figure it out. Commander gives you actual control:

proc, _ := cmd.Start(ctx, "some-daemon")
// Let it run for a bit
time.Sleep(5 * time.Second)
// Try to shut it down nicely, then murder it if necessary
err := proc.Stop(ctx, 10*time.Second) // SIGTERM, then SIGKILL after 10s
if errors.Is(err, commonerrors.ErrTerminated) {
    log.Println("Process terminated gracefully")
} else if errors.Is(err, commonerrors.ErrKilled) {
    log.Println("Process had to be put down")
}

This is how process management should work – you tell it what you want, and it fucking does it.

Testing Without Losing Your Mind

The real kicker? Commander comes with a comprehensive mocking system that doesn’t make you want to set your computer on fire:

func TestDeployScript(t *testing.T) {
    mock := commander.NewMock()
    defer mock.VerifyExpectations()
    // Set up expectations
    mock.Expect("docker", "build", "-t", "myapp", ".").ReturnOutput([]byte("Build successful"))
    mock.Expect("docker", "push", "myapp").ReturnError(errors.New("network timeout"))
    // Test your function that uses Commander
    err := deployApp(mock)
    assert.Error(t, err) // Should fail on push
}

You can mock argument patterns with regex, test concurrent execution, verify call order – all the shit you need for proper testing without writing a novel’s worth of setup code.

Why This Matters

Look, I get it. There are other command execution libraries out there. But most of them either:

  1. Solve half the problem and leave you to figure out the rest
  2. Add so much abstraction you need a fucking manual to run ls
  3. Break in weird edge cases that only happen in production at 3 AM

Commander solves the actual problems developers have when dealing with external processes:

  • Streaming output in real-time (because batch processing is for cowards)
  • Proper timeout handling (because infinite loops are not a feature)
  • Graceful process termination (because kill -9 should be a last resort)
  • Comprehensive error reporting (because “something went wrong” is not helpful)
  • Testable code (because untested code is broken code)

The Technical Bits

Under the hood, Commander handles all the bullshit you don’t want to think about:

  • Thread-safe operations with proper mutex usage
  • Context cancellation that actually works
  • Signal handling that detects SIGTERM vs SIGKILL
  • Channel management that doesn’t leak goroutines
  • Error wrapping with proper context

It’s built on top of the same os/exec primitives, but wrapped in an API designed by someone who actually uses this stuff in production.

Get Started or Get Out

Installation is the usual Go bullshit:

go get github.com/psyb0t/commander

The README has examples for every scenario you can think of, written in a style that won’t put you to sleep.

If you’re tired of fighting with Go’s command execution and want something that just works, give Commander a shot. If you’re happy writing boilerplate garbage and handling edge cases manually, stick with os/exec and enjoy your suffering.

Final Thoughts

Building tools is about solving real problems for real people. Commander exists because I got sick of writing the same error-prone bullshit over and over again. It’s designed for developers who want to get shit done without wrestling with the standard library’s quirks.