runutil

package module
v0.2.6 Latest Latest
Warning

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

Go to latest
Published: Dec 19, 2025 License: MIT Imports: 17 Imported by: 4

README

GoDoc

runutil

A Go utility library for executing external commands and processes with support for piping data between Go native functions and UNIX commands.

Features

  • Simple command execution with proper error handling
  • Pipeline support for chaining commands like Unix shell pipes
  • Custom environments for running commands with specific environment variables
  • JSON output parsing for commands that return structured data
  • Process inspection (Linux) for querying process state and information
  • Process discovery (Linux) for finding processes by name
  • Zombie process cleanup for applications that spawn many child processes

Installation

go get github.com/KarpelesLab/runutil

Quick Start

Basic Command Execution
// Run a command with output to stdout
err := runutil.Run("ls", "-la")

// Capture command output as bytes
output, err := runutil.RunGet("date")

// Parse JSON output from a command
var data MyStruct
err := runutil.RunJson(&data, "kubectl", "get", "pods", "-o", "json")
Piping Data

The library excels at creating complex pipelines, mixing Go native methods with external commands:

// Compress data using gzip
compressed, err := runutil.RunPipe(input, "gzip", "-9")
if err != nil {
    return err
}
defer compressed.Close()

// Decompress using Go's gzip package
reader, err := gzip.NewReader(compressed)
Reading Command Output
// Read command output as a stream
pipe, err := runutil.RunRead("cat", "/var/log/syslog")
if err != nil {
    return err
}
defer pipe.Close()

// Process the stream
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}
Custom Environments
// Create a custom environment
env := runutil.NewEnv("/home/myuser", "MY_VAR=value", "DEBUG=true")

// Run command with custom environment
err := env.Run("my-command", "--flag")

// All Run* functions are available as methods on Env
output, err := env.RunGet("printenv", "MY_VAR")
Environment Manipulation
env := runutil.NewEnv("/home/user")

// Set or update a variable
env.Set("DATABASE_URL", "postgres://localhost/mydb")

// Get a variable
value := env.Get("PATH")

// Check if a variable exists
if env.Contains("DEBUG") {
    // ...
}

// Remove a variable
env.Unset("TEMP_VAR")

// Merge environments
combined := env.Join(otherEnv).Dedup()
Shell Commands (Unix only)
// Execute a shell command
err := runutil.Sh("echo 'Hello' | grep -o 'H.*'")

// Safely quote user input for shell commands
userInput := "file with 'quotes' and spaces"
cmd := "cat " + runutil.ShQuote(userInput)
err := runutil.Sh(cmd)
Process Inspection (Linux only)
// Get process state by PID
state, err := runutil.PidState(1234)
if err != nil {
    return err
}

if state.IsRunning() {
    started, err := state.Started()
    fmt.Printf("Process started at: %v\n", started)
}

// Get detailed Linux process info
linuxState, err := runutil.LinuxPidState(1234)
fmt.Printf("Process %s has %d threads\n", linuxState.Comm, linuxState.NumThreads)
Process Discovery (Linux only)
// Find all PIDs for a process name
pids := runutil.PidOf("nginx")
for _, pid := range pids {
    fmt.Printf("Found nginx with PID: %d\n", pid)
}

// Get command-line arguments of a process
args, err := runutil.ArgsOf(pids[0])
fmt.Printf("nginx args: %v\n", args)
Zombie Process Cleanup
// Clean up zombie child processes
// Useful when running as PID 1 or spawning many short-lived processes
err := runutil.Reap()

Error Handling

Unlike standard os/exec, runutil properly propagates errors from commands through pipes. If a command in a pipeline fails, the error is returned when reading from the pipe at EOF:

pipe, _ := runutil.RunPipe(input, "nonexistent-command")
defer pipe.Close()

// The error from the failed command will be returned here
_, err := io.ReadAll(pipe)
if err != nil {
    // err contains the command's exit error
}

Resource Management

When using RunRead or RunPipe, always close the returned Pipe to prevent zombie processes:

pipe, err := runutil.RunRead("long-running-command")
if err != nil {
    return err
}
defer pipe.Close() // Always close!

// Process data...

For graceful shutdown with a timeout:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := pipe.CloseWait(ctx) // Waits up to 5 seconds, then kills

Troubleshooting

Zombie Processes

If you see zombie processes accumulating, it means Wait() was not called on child processes. This can happen if:

  1. You didn't close a Pipe returned by RunRead or RunPipe
  2. You didn't read the pipe to EOF

Solutions:

  • Always use defer pipe.Close() after getting a Pipe
  • Read the pipe completely to EOF
  • Call Reap() periodically if running many short-lived commands
Process Not Found

On non-Linux systems, PidOf, ArgsOf, and PidState return empty results or ErrNotSupported. These functions rely on the /proc filesystem which is Linux-specific.

Platform Support

Feature Linux macOS Windows
Run, RunGet, RunPipe, etc. Yes Yes Yes
Sh, ShQuote Yes Yes No
PidOf, ArgsOf Yes No No
PidState, LinuxPidState Yes No No
Reap Yes Yes No-op

License

MIT License - see LICENSE file for details.

Documentation

Overview

Package runutil provides utilities for executing external commands and processes with support for piping data between Go native functions and UNIX commands.

This library makes it easy to execute complex sequences of executables, mixing both Go native methods like gzip.NewReader and UNIX commands in the way pipes work in bash and other shells.

Basic Command Execution

The simplest way to run a command is with the Run function:

err := runutil.Run("ls", "-la")

To capture output, use RunGet:

output, err := runutil.RunGet("date")

Piping

The library excels at creating complex pipelines. Use RunPipe to connect an input stream to a command and get its output:

compressed, err := runutil.RunPipe(input, "gzip", "-9")
if err != nil {
    return err
}
// compressed is now a Pipe that can be read or passed to another command

The returned Pipe interface extends io.ReadCloser with additional methods for proper resource management.

Environment Management

Commands can be run with custom environments using the Env type:

env := runutil.NewEnv("/home/user", "CUSTOM_VAR=value")
err := env.Run("my-command")

Error Handling

Unlike standard os/exec, runutil properly propagates errors from commands. If a command in a pipeline fails, the error is returned when reading from the pipe, ensuring no errors are silently ignored.

Resource Management

When using RunRead or RunPipe, always ensure the returned Pipe is closed to prevent zombie processes:

pipe, err := runutil.RunRead("long-running-command")
if err != nil {
    return err
}
defer pipe.Close()
// read from pipe...

For graceful shutdown with a timeout, use CloseWait:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := pipe.CloseWait(ctx)

Process Inspection (Linux)

On Linux, the package provides functions for process discovery and state inspection:

pids := runutil.PidOf("nginx")
state, err := runutil.PidState(uint64(pids[0]))
if err == nil && state.IsRunning() {
    started, _ := state.Started()
    fmt.Printf("Process started at: %v\n", started)
}

Zombie Process Cleanup

The Reap function can be called periodically to clean up zombie child processes:

err := runutil.Reap()

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrCommandMissing is returned when a Run function is called with no arguments.
	ErrCommandMissing = errors.New("command is missing")

	// ErrNotSupported is returned when a platform-specific operation is called
	// on an unsupported platform (e.g., PidState on non-Linux systems).
	ErrNotSupported = errors.New("operation not supported on this platform")
)

Package-level errors returned by runutil functions.

Functions

func ArgsOf added in v0.2.5

func ArgsOf(pid int) ([]string, error)

ArgsOf returns the command-line arguments of the process with the given PID. It reads /proc/<pid>/cmdline and splits it on null bytes to get individual arguments. The first element is typically the program name. Returns an error if the process does not exist or cannot be read. This function is only available on Linux.

func PidOf added in v0.1.0

func PidOf(name string) (res []int)

PidOf finds all process IDs whose executable name matches the given name. It searches /proc for all running processes and checks both /proc/<pid>/cmdline and /proc/<pid>/exe to find matches. Returns an empty slice if no matches are found. This function is only available on Linux.

func Reap added in v0.0.4

func Reap() error

Reap cleans up zombie child processes by repeatedly calling wait4 with WNOHANG until no more terminated children remain. This is useful when running as PID 1 (init) or when spawning many short-lived child processes that may become zombies. On Unix systems, zombie processes consume a process table entry until their parent calls wait; this function releases those entries.

func Run

func Run(arg ...string) error

Run executes a command and waits for it to complete. The command's stdout and stderr are forwarded to os.Stdout and os.Stderr. The first argument is the command name, and subsequent arguments are passed to the command. Returns ErrCommandMissing if no arguments are provided.

func RunGet

func RunGet(arg ...string) ([]byte, error)

RunGet executes a command and returns its stdout as a byte slice. The function waits for the command to complete before returning. This is useful for capturing command output for further processing. Returns ErrCommandMissing if no command arguments are provided.

func RunJson

func RunJson(obj interface{}, arg ...string) error

RunJson executes a command and decodes its JSON stdout into the provided object. The obj parameter should be a pointer to the type you want to decode into. This is useful for commands that output structured JSON data. Returns ErrCommandMissing if no command arguments are provided.

func RunWrite

func RunWrite(r io.Reader, arg ...string) error

RunWrite executes a command with the provided Reader as stdin. The command's stdout and stderr are forwarded to os.Stdout and os.Stderr. The function waits for the command to complete before returning. Returns ErrCommandMissing if no command arguments are provided.

func Sh

func Sh(cmd string) error

Sh executes a shell command using /bin/sh -c. This is only available on Unix-like systems (Linux, macOS, etc.). The command string is passed directly to the shell and can include pipes, redirects, and other shell features.

func ShQuote

func ShQuote(s string) string

ShQuote safely quotes a string for use in shell commands. It wraps the string in single quotes and properly escapes any embedded single quotes using the '\” technique. This is useful for safely interpolating user input into shell commands.

Example:

cmd := "echo " + ShQuote(userInput)
Sh(cmd)

Types

type Env added in v0.2.3

type Env []string

Env represents a set of environment variables as a slice of "KEY=VALUE" strings. A nil Env indicates that the system's environment should be used.

func NewEnv added in v0.2.3

func NewEnv(home string, vars ...string) Env

NewEnv creates a new Env with standard environment variables set. It initializes USER (derived from home path), PWD ("/"), HOME, and PATH with sensible defaults. Additional variables can be passed as "KEY=VALUE" strings. Duplicate keys are automatically deduplicated, keeping the last value.

func SysEnv added in v0.2.3

func SysEnv() Env

SysEnv returns a nil Env, which signals that the operating system's environment should be used when running commands. This is the default behavior and is more efficient than copying the system environment.

func (Env) Contains added in v0.2.3

func (e Env) Contains(k string) bool

Contains reports whether the Env contains the specified key. If the Env is nil, it checks the system environment.

func (Env) Dedup added in v0.2.3

func (e Env) Dedup() Env

Dedup returns a copy of the Env with duplicate keys removed. When duplicates exist, the last occurrence is kept, preserving the behavior of later values overriding earlier ones. Returns nil if the input Env is nil.

func (Env) Get added in v0.2.3

func (e Env) Get(k string) string

Get retrieves the value of an environment variable by key. Returns an empty string if the key is not found. If the Env is nil, it queries the system environment via os.Getenv.

func (Env) Join added in v0.2.3

func (e Env) Join(others ...Env) Env

Join merges multiple environments together into a new Env. If the receiver is nil, it starts with the system environment. No deduplication is performed; use Dedup afterwards if needed.

func (Env) Run added in v0.2.3

func (e Env) Run(arg ...string) error

Run executes a command with the specified environment and waits for it to complete. The command's stdout and stderr are forwarded to os.Stdout and os.Stderr. If the Env is nil, the system environment is used. Returns ErrCommandMissing if no arguments are provided.

func (Env) RunGet added in v0.2.3

func (e Env) RunGet(arg ...string) ([]byte, error)

RunGet executes a command with the specified environment and returns its stdout as a byte slice. The function waits for the command to complete before returning. Returns ErrCommandMissing if no command arguments are provided.

func (Env) RunJson added in v0.2.3

func (e Env) RunJson(obj interface{}, arg ...string) error

RunJson executes a command with the specified environment and decodes its JSON stdout into the provided object. The obj parameter should be a pointer to the type you want to decode into. Returns ErrCommandMissing if no command arguments are provided.

func (Env) RunPipe added in v0.2.3

func (e Env) RunPipe(r io.Reader, arg ...string) (Pipe, error)

RunPipe executes a command with the specified environment in the background with both stdin and stdout connected. Data from the provided Reader is passed to the command's stdin, and the command's stdout is available through the returned Pipe. Always close the returned Pipe to release resources and prevent zombie processes. Returns ErrCommandMissing if no command arguments are provided.

func (Env) RunRead added in v0.2.3

func (e Env) RunRead(arg ...string) (Pipe, error)

RunRead executes a command with the specified environment in the background and returns its stdout as a Pipe. Always close the returned Pipe to release resources and prevent zombie processes. If the command fails, the error is returned when reading from the pipe at EOF. Returns ErrCommandMissing if no command arguments are provided.

func (Env) RunWrite added in v0.2.3

func (e Env) RunWrite(r io.Reader, arg ...string) error

RunWrite executes a command with the specified environment and the provided Reader as stdin. The command's stdout and stderr are forwarded to os.Stdout and os.Stderr. The function waits for the command to complete before returning. Returns ErrCommandMissing if no command arguments are provided.

func (*Env) Set added in v0.2.3

func (e *Env) Set(k, v string)

Set sets or updates an environment variable in the Env. If the Env is nil, it is first initialized with the system environment. If the key already exists, its value is updated in place.

func (*Env) Unset added in v0.2.3

func (e *Env) Unset(k string)

Unset removes all instances of a given key from the Env. If the Env is nil, it is first initialized with the system environment.

type LinuxProcState added in v0.2.0

type LinuxProcState struct {
	Pid         int    // Process ID
	Comm        string // Executable name (e.g., "bash")
	State       byte   // Process state: 'R' (running), 'S' (sleeping), 'D' (disk sleep), 'Z' (zombie), 'T' (stopped), etc.
	PPid        int    // Parent process ID
	PGrp        int    // Process group ID
	Session     int    // Session ID
	TtyNr       int    // Controlling terminal
	Tpgid       int    // Foreground process group ID of controlling terminal
	Flags       uint   // Kernel flags (PF_*)
	Minflt      uint64 // Minor faults (no disk I/O)
	Cminflt     uint64 // Minor faults by waited-for children
	Majflt      uint64 // Major faults (required disk I/O)
	Cmajflt     uint64 // Major faults by waited-for children
	Utime       uint64 // User mode CPU time in clock ticks
	Stime       uint64 // Kernel mode CPU time in clock ticks
	Cutime      uint64 // User mode CPU time of waited-for children
	Cstime      uint64 // Kernel mode CPU time of waited-for children
	Priority    int64  // Scheduling priority
	Nice        int64  // Nice value: 19 (low priority) to -20 (high priority)
	NumThreads  int64  // Number of threads in the process
	Itrealvalue int64  // Time in jiffies before the next SIGALRM
	StartTime   uint64 // Process start time after system boot (in clock ticks)
	Vsize       uint64 // Virtual memory size in bytes
	RSS         int64  // Resident set size (pages in real memory)
	RSSlim      uint64 // Current soft limit on RSS in bytes
}

LinuxProcState contains detailed process information parsed from /proc/<pid>/stat. This is only available on Linux systems. The fields correspond to the columns documented in the procfs(5) man page.

func LinuxPidState added in v0.2.0

func LinuxPidState(pid uint64) (*LinuxProcState, error)

LinuxPidState returns detailed Linux-specific process information for the given PID. It reads and parses /proc/<pid>/stat to populate the LinuxProcState struct. Returns an error if the process does not exist or the stat file cannot be parsed.

func (*LinuxProcState) IsRunning added in v0.2.0

func (s *LinuxProcState) IsRunning() bool

IsRunning reports whether the process is currently in the running state ('R'). Note that 'R' means the process is either running or runnable (in the run queue).

func (*LinuxProcState) Started added in v0.2.0

func (s *LinuxProcState) Started() (time.Time, error)

Started calculates and returns the actual time when the process was started. It uses /proc/uptime to convert the relative start time to an absolute timestamp.

type Pipe added in v0.0.4

type Pipe interface {
	io.ReadCloser

	// CopyTo copies all data from the pipe to the provided Writer.
	// It returns the number of bytes copied and any error encountered.
	// The process exit status is checked after all data is read.
	CopyTo(io.Writer) (int64, error)

	// CloseWait closes the pipe and waits for the process to exit.
	// If the context has a deadline, the process is killed when the
	// deadline expires. This allows for graceful shutdown with a timeout.
	CloseWait(ctx context.Context) error
}

Pipe extends io.ReadCloser with additional methods for managing process output streams. It is returned by RunRead and RunPipe.

Unlike a standard io.ReadCloser, a Pipe properly handles process lifecycle and error propagation. When the underlying process fails, the error is returned on the final Read call (at EOF), ensuring that command failures are not silently ignored.

func RunPipe

func RunPipe(r io.Reader, arg ...string) (Pipe, error)

RunPipe executes a command in the background with both stdin and stdout connected. Data from the provided Reader is passed to the command's stdin, and the command's stdout is available through the returned Pipe. This enables chaining commands in a pipeline, similar to Unix shell pipes. Always close the returned Pipe to release resources and prevent zombie processes. Returns ErrCommandMissing if no command arguments are provided.

func RunRead

func RunRead(arg ...string) (Pipe, error)

RunRead executes a command in the background and returns its stdout as a Pipe. The command continues running until the pipe is closed or EOF is reached. Always close the returned Pipe to release resources and prevent zombie processes. If the command fails, the error is returned when reading from the pipe at EOF. Returns ErrCommandMissing if no command arguments are provided.

type ProcState added in v0.2.0

type ProcState interface {
	// IsRunning reports whether the process is currently running (state 'R').
	IsRunning() bool

	// Started returns the time when the process was started.
	Started() (time.Time, error)
}

ProcState provides information about a running process. On Linux, this is implemented by LinuxProcState with full process details. On other platforms, PidState returns ErrNotSupported.

func PidState added in v0.2.0

func PidState(pid uint64) (ProcState, error)

PidState returns process state information for the given PID. On Linux, this returns a *LinuxProcState which implements ProcState. Returns an error if the process does not exist or cannot be read.

Jump to

Keyboard shortcuts

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