sho

package module
v0.0.0-...-b4b8de0 Latest Latest
Warning

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

Go to latest
Published: Feb 26, 2026 License: EUPL-1.2 Imports: 11 Imported by: 0

README

将 (Shō)

A simple, extensible task runner for automating system updates and custom tasks using Go.

Features

  • Simple task registration
  • Multiple task types, shell commands or Go functions
  • Selective execution, run tasks by name, by tag, or mix and match
  • Support for interactive commands, proxies stdin/stdout/stderr for interactive prompts (like sudo passwords)
  • Type-safe, IDE support, tools - it's Go all the way down
  • Run tasks sequentially or in parallel

Quick Start

Install the sho CLI tool:

go install codeberg.org/zerodeps/sho/cmd/sho@latest

Scaffold a tasks directory in your project:

sho init

This creates a tasks/ directory with its own go.mod that imports the sho library, plus a starter tasks.go template. Edit tasks/tasks.go to define your tasks, then run them:

cd tasks && go run .           # list all tasks
cd tasks && go run . hello     # run the "hello" task
cd tasks && go run . -a        # run all tasks

sho CLI

The sho CLI provides quality-of-life tooling for working with the task runner.

Installation
go install codeberg.org/zerodeps/sho/cmd/sho@latest
Commands
sho init

Scaffolds a tasks/ directory in the current project:

sho init
  • Creates tasks/go.mod (module tasks, imports codeberg.org/zerodeps/sho)
  • Downloads tasks/main.go entry-point (skipped if one already exists)
  • Downloads a starter tasks/tasks.go template (skipped if one already exists)
  • Runs go mod tidy to resolve the sho dependency

To reset the template:

rm tasks/tasks.go && sho init

Usage

Registering Tasks

Tasks are defined in one or more .go files and registered in each file's init() function:

// tasks.go
package main

func init() {
    Register(
        // Shell command task
        Task{
            Name:        "apt-update",
            Description: "Update APT packages",
            Command:     ShellCommand{"sudo", "sh", "-c", "apt update && apt upgrade -y"},
            Tags:        []string{"system"},
        },
        // Go function task
        Task{
            Name:        "custom",
            Description: "Custom task",
            Command: FuncCommand(func() error {
                // Your custom Go code here
                return nil
            }),
        },
    )
}

Task, ShellCommand, FuncCommand, and Register are provided as aliases in the generated main.go so task files need no extra imports.

See the examples folder.

Running Tasks
cd tasks && go run . [flags] [name...]

Each positional name is resolved using the following logic:

  1. If the name matches a registered task, that task is run.
  2. Otherwise, if it matches a registered tag, all tasks carrying that tag are run.
  3. If it matches neither, an error is returned.

Use flags to skip the fallback and enforce a specific interpretation:

Flag Description
-l, --list List all tasks and tags
-a, --all Run all tasks in alphabetical order
--task Force name to be interpreted as a task (repeatable)
-t, --tag Force name to be interpreted as a tag (repeatable)
-p, --parallel Run tasks in parallel
# Smart resolution: runs "build" task, then expands "ci" tag
cd tasks && go run . build ci

# Force task-only: error if "ci" is not a task name
cd tasks && go run . --task ci

# Force tag-only: error if "build" is not a tag name
cd tasks && go run . --tag build
Task Types
Shell Commands
Register(Task{
    Name:        "hello",
    Description: "Say hello",
    Command:     ShellCommand{"echo", "Hello, World!"},
})
Go Functions
Register(Task{
    Name:        "complex",
    Description: "Complex task",
    Command: FuncCommand(func() error {
        // Complex logic here
        fmt.Println("Doing something complex")
        return nil
    }),
})

How It Works

  1. Initialization - All init() functions run before main(), registering tasks
  2. Filtering - main() resolves names to tasks: positional args match task names first, then tag names. --task enforces task-only lookup; --tag/-t enforces tag-only lookup
  3. Execution - The task selection is passed to a Runner to run sequentially or in parallel
  4. Error Handling: Failed tasks are captured and handled gracefully

License

EUPL v1.2

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

Examples

Constants

This section is empty.

Variables

View Source
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

func PrintFlags(fset *flag.FlagSet, out io.Writer)

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

type Flags struct {
	All      bool
	List     bool
	Parallel bool
	Tags     []string
	Tasks    []string
}

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

func SetupFlags(fset *flag.FlagSet) *Flags

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

func (Flags) String

func (f Flags) String() string

String returns the flag values as a space-separated CLI argument string, using short flag names. Only flags set to a non-zero value are included.

Implements fmt.Stringer.

Usage:

fmt.Println(flg)              // "-a -p -t ci"
strings.Fields(flg.String())  // []string{"-a", "-p", "-t", "ci"}

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

func (r *Runner) Register(tasks ...Task) error

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

func (r *Runner) Run(taskNames ...string) error

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

func (r *Runner) RunParallel(taskNames ...string) error

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

func (r *Runner) Tag(name string) []Task

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

func (r *Runner) Tags() func(yield func(string) bool)

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

func (r *Runner) Task(name string) (Task, bool)

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

func (r *Runner) Tasks() func(yield func(Task) bool)

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.

Directories

Path Synopsis
cmd
sho command
Package main provides the sho CLI tool, a quality-of-life companion for the shō task runner.
Package main provides the sho CLI tool, a quality-of-life companion for the shō task runner.

Jump to

Keyboard shortcuts

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