Documentation
¶
Overview ¶
Package sho provides shō, a simple, extensible task runner for automating system updates and custom tasks.
Tasks are defined using the Command interface and registered via init() functions in a tasks/ subdirectory. The runner supports both shell commands and Go functions, with full interactive terminal support for commands like sudo.
Typical usage in a tasks/ directory:
go run . # list all registered tasks (implicit) go run . -l # list all registered tasks (explicit) go run . task1 task2 # run specific tasks go run . -a # run all tasks
Index ¶
- Variables
- func Main()
- func PrintFlags(fset *flag.FlagSet, out io.Writer)
- func Register(tasks ...Task)
- type Command
- type Flags
- type FuncCommand
- type Runner
- func (r *Runner) Register(tasks ...Task) error
- func (r *Runner) Run(taskNames ...string) error
- func (r *Runner) RunParallel(taskNames ...string) error
- func (r *Runner) Tag(name string) []Task
- func (r *Runner) Tags() func(yield func(string) bool)
- func (r *Runner) Task(name string) (Task, bool)
- func (r *Runner) Tasks() func(yield func(Task) bool)
- type ShellCommand
- type Task
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrNoTasksProvided = errors.New("no tasks provided") ErrTaskNameEmpty = errors.New("task name cannot be empty") ErrTaskNameWhitespace = errors.New("task name cannot contain whitespace") ErrTaskCommandNil = errors.New("task command cannot be nil") ErrTagNameEmpty = errors.New("tag name cannot be empty") ErrTagNameWhitespace = errors.New("tag name cannot contain whitespace") ErrTaskDuplicate = errors.New("task already registered") )
Functions ¶
func Main ¶
func Main()
Main is the CLI entry point for a shō task runner.
Call this from the main function of your tasks directory:
func main() { sho.Main() }
func PrintFlags ¶
PrintFlags writes the formatted flag list from fset to out.
Each flag is printed with a single-dash prefix for one-letter names and a double-dash prefix for longer names. Flags with no default value show a "value" placeholder.
Usage:
sho.PrintFlags(fset, fset.Output())
func Register ¶
func Register(tasks ...Task)
Register registers a task with the default runner.
Panics if registration fails (e.g., duplicate task name, invalid name). This is intended for use in init() functions where errors cannot be returned.
Usage:
func init() {
Register(Task{
Name: "deploy",
Description: "Deploy to production",
Command: ShellCommand{"./deploy.sh"},
Tags: []string{"production"},
})
}
Types ¶
type Command ¶
type Command interface {
Run() error
}
Command is the interface that wraps the Run method.
Run executes the command and returns any error that occurred. Implementations should connect to os.Stdin, os.Stdout, and os.Stderr to support interactive commands.
type Flags ¶
Flags holds the values for all shō runner CLI flags after parsing.
It is returned by SetupFlags and populated once the associated FlagSet is parsed.
func SetupFlags ¶
SetupFlags registers all shō runner CLI flags on fset and returns a *Flags whose fields are populated once fset.Parse is called.
It does not configure fset.Usage; callers should set their own.
Usage:
fset := flag.NewFlagSet("tasks", flag.ExitOnError)
flg := sho.SetupFlags(fset)
fset.Parse(os.Args[1:])
// flg.All, flg.List, flg.Parallel, flg.Tags, flg.Tasks are now set
type FuncCommand ¶
type FuncCommand func() error
FuncCommand is a Command implementation that wraps a Go function.
This allows tasks to be defined as arbitrary Go code rather than shell commands.
Example:
cmd := FuncCommand(func() error {
fmt.Println("Task executed")
return nil
})
Example ¶
task := FuncCommand(func() error {
fmt.Println("Task executed")
return nil
})
task.Run()
Output: Task executed
func (FuncCommand) Run ¶
func (f FuncCommand) Run() error
Run executes the wrapped function.
Returns error from wrapped function.
Usage:
cmd := FuncCommand(func() error {
return performComplexOperation()
})
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner manages a collection of named tasks and executes them on demand.
Tasks can be registered via Register and executed via Run. The zero value is ready to use (maps are lazily initialized).
Example (Run) ¶
r := NewRunner()
// Register multiple tasks
_ = r.Register(Task{
Name: "task1",
Description: "First task",
Command: ShellCommand{"echo", "Task 1"},
})
_ = r.Register(Task{
Name: "task2",
Description: "Second task",
Command: ShellCommand{"echo", "Task 2"},
})
// Run only task1
r.Run("task1")
Output: Executing tasks... [1/1] task1: First task $ echo Task 1 Task 1 ✓ task1 completed successfully Summary: 1 succeeded
Example (Run_all) ¶
r := NewRunner()
// Register tasks
_ = r.Register(Task{
Name: "hello",
Description: "Say hello",
Command: ShellCommand{"echo", "Hello"},
})
_ = r.Register(Task{
Name: "world",
Description: "Say world",
Command: ShellCommand{"echo", "World"},
})
// Run all tasks by collecting task names from iterator
var allTasks []string
for task := range r.Tasks() {
allTasks = append(allTasks, task.Name)
}
r.Run(allTasks...)
Output: Executing tasks... [1/2] hello: Say hello $ echo Hello Hello ✓ hello completed successfully [2/2] world: Say world $ echo World World ✓ world completed successfully Summary: 2 succeeded
Example (Run_failure) ¶
r := NewRunner()
// Register a task that will fail
_ = r.Register(Task{
Name: "fail",
Description: "Failing task",
Command: ShellCommand{"false"},
})
// Run the failing task
r.Run("fail")
Output: Executing tasks... [1/1] fail: Failing task $ false Summary: 0 succeeded, 1 failed
func NewRunner ¶
func NewRunner() *Runner
NewRunner creates a new Runner with empty task and tag registries.
Usage:
r := NewRunner()
r.Register(Task{
Name: "build",
Description: "Build the project",
Command: ShellCommand{"make", "build"},
})
r.Run("build")
func (*Runner) Register ¶
Register adds one or more tasks to the runner.
All tasks are attempted; errors from invalid tasks are collected and returned together as a joined error, so callers see every problem in a single call. Valid tasks are registered even if others in the same call fail.
The task name must be non-empty, free of whitespace, and unique within this runner. Description and Tags are optional. If tags are provided, tag names must be non-empty and free of whitespace.
Returns ErrNoTasksProvided if no tasks are provided. Returns ErrTaskNameEmpty if a task name is empty. Returns ErrTaskNameWhitespace if a task name contains whitespace. Returns ErrTaskCommandNil if a task command is nil. Returns ErrTagNameEmpty if a tag name is empty. Returns ErrTagNameWhitespace if a tag name contains whitespace. Returns ErrTaskDuplicate if a task with the same name is already registered.
Usage:
r := NewRunner()
err := r.Register(
Task{
Name: "build",
Description: "Build the project",
Command: ShellCommand{"go", "build", "./..."},
},
Task{
Name: "deploy",
Description: "Deploy to production",
Command: ShellCommand{"./deploy.sh"},
Tags: []string{"production", "critical"},
},
)
if err != nil {
log.Fatal(err)
}
Example (Function) ¶
r := NewRunner()
// Register a function command task
_ = r.Register(Task{
Name: "greet",
Description: "Greet user",
Command: FuncCommand(func() error {
fmt.Println("Greetings from Go function!")
return nil
}),
})
// Execute via run
r.Run("greet")
Output: Executing tasks... [1/1] greet: Greet user Greetings from Go function! ✓ greet completed successfully Summary: 1 succeeded
Example (Shell) ¶
r := NewRunner()
// Register a shell command task
_ = r.Register(Task{
Name: "hello",
Description: "Say hello",
Command: ShellCommand{"echo", "Hello from task"},
})
// Execute via run
r.Run("hello")
Output: Executing tasks... [1/1] hello: Say hello $ echo Hello from task Hello from task ✓ hello completed successfully Summary: 1 succeeded
Example (Validation) ¶
r := NewRunner()
// Attempt to register task with empty name
err := r.Register(Task{
Name: "",
Description: "Empty name task",
Command: ShellCommand{"echo", "test"},
})
if err != nil {
fmt.Println("Error:", err)
}
// Attempt to register task with whitespace in name
err = r.Register(Task{
Name: "my task",
Description: "Whitespace name",
Command: ShellCommand{"echo", "test"},
})
if err != nil {
fmt.Println("Error:", err)
}
Output: Error: task name cannot be empty Error: task name "my task": task name cannot contain whitespace
func (*Runner) Run ¶
Run executes the specified tasks sequentially.
At least one task name must be provided. Tasks run with full terminal access (stdin/stdout/stderr connected). Failed tasks are logged in red to stderr, but execution continues with remaining tasks.
Returns error if no task names provided. Returns errors.Join of all task failures (including not found errors).
Usage:
r := NewRunner()
r.Register(Task{Name: "test", Description: "Run tests", Command: ShellCommand{"go", "test"}})
r.Register(Task{Name: "build", Description: "Build", Command: ShellCommand{"go", "build"}})
if err := r.Run("test", "build"); err != nil {
log.Fatal(err)
}
func (*Runner) RunParallel ¶
RunParallel executes the specified tasks concurrently.
At least one task name must be provided. Tasks run in parallel, with the Go runtime managing scheduling across available CPUs. Each task gets full terminal access, though output may be interleaved. Failed tasks are collected and returned as a joined error.
Returns error if no task names provided. Returns errors.Join of all task failures (including not found errors).
Usage:
r := NewRunner()
r.Register(Task{Name: "lint", Description: "Lint", Command: ShellCommand{"golint"}})
r.Register(Task{Name: "test", Description: "Test", Command: ShellCommand{"go", "test"}})
if err := r.RunParallel("lint", "test"); err != nil {
log.Fatal(err)
}
Example ¶
r := NewRunner()
// Register multiple tasks
_ = r.Register(Task{
Name: "task1",
Description: "First task",
Command: ShellCommand{"echo", "Task 1"},
})
_ = r.Register(Task{
Name: "task2",
Description: "Second task",
Command: ShellCommand{"echo", "Task 2"},
})
_ = r.Register(Task{
Name: "task3",
Description: "Third task",
Command: ShellCommand{"echo", "Task 3"},
})
// Run tasks in parallel
// Note: Output order is non-deterministic in parallel execution
r.RunParallel("task1", "task2", "task3")
// Output varies due to parallel execution
func (*Runner) Tag ¶
Tag returns all tasks with the specified tag.
Tasks are returned in registration order as Task values (copied to prevent modification). Returns nil if the tag doesn't exist.
Usage:
r := NewRunner()
r.Register(Task{Name: "test", Description: "Test", Command: ShellCommand{"go", "test"}, Tags: []string{"ci"}})
tasks := r.Tag("ci")
for _, task := range tasks {
fmt.Println(task.Name)
}
func (*Runner) Tags ¶
Tags returns an iterator over all registered tag names.
The iterator yields tag names in sorted order.
Usage:
r := NewRunner()
r.Register(Task{Name: "t1", Description: "Task 1", Command: ShellCommand{"echo"}, Tags: []string{"quick"}})
for tag := range r.Tags() {
fmt.Println(tag)
}
func (*Runner) Task ¶
Task returns the task with the specified name.
Returns the task by value (with cloned Tags slice) to prevent modification. Returns a zero-value Task and false if the task doesn't exist.
Usage:
r := NewRunner()
r.Register(Task{Name: "build", Description: "Build project", Command: ShellCommand{"make"}})
if task, ok := r.Task("build"); ok {
fmt.Println(task.Description)
}
func (*Runner) Tasks ¶
Tasks returns an iterator over all registered tasks.
The iterator yields Task values in sorted order by name. Task values are returned by value to prevent modification.
Usage:
r := NewRunner()
r.Register(Task{Name: "build", Description: "Build", Command: ShellCommand{"make"}})
for task := range r.Tasks() {
fmt.Printf("%s: %s\n", task.Name, task.Description)
}
Example ¶
r := NewRunner()
// Register tasks
_ = r.Register(Task{
Name: "hello",
Description: "Say hello",
Command: ShellCommand{"echo", "Hello"},
})
_ = r.Register(Task{
Name: "goodbye",
Description: "Say goodbye",
Command: ShellCommand{"echo", "Goodbye"},
})
// Iterate over all tasks
for task := range r.Tasks() {
fmt.Printf("%s: %s\n", task.Name, task.Description)
}
Output: goodbye: Say goodbye hello: Say hello
type ShellCommand ¶
type ShellCommand []string
ShellCommand represents a shell command as a slice of strings.
The first element is the command and remaining elements are arguments.
Example:
cmd := ShellCommand{"echo", "Hello, World!"}
Example ¶
cmd := ShellCommand{"echo", "Hello, World!"}
cmd.Run()
Output: $ echo Hello, World! Hello, World!
func (ShellCommand) Run ¶
func (s ShellCommand) Run() error
Run executes the shell command with full terminal access.
It prints the command being executed, then runs it with stdin/stdout/stderr connected to the terminal to support interactive commands like sudo.
Returns error if command slice is empty. Returns error from exec.Command.Run if command execution fails.
Usage:
cmd := ShellCommand{"ls", "-la"}
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
type Task ¶
type Task struct {
Name string
Description string // Optional - empty string is valid
Command Command
Tags []string // Optional - nil or empty slice is valid
}
Task represents a task that can be registered and executed by a Runner.
Tasks are identified by name and can optionally be organized with tags.