resenje.org/command

A simple, declarative Go package for building tree-based CLI commands with zero dependencies.


License
BSD-3-Clause
Install
go get resenje.org/command

Documentation

Command

GoDoc Go Go Report Card NewReleases

Package command provides a simple, declarative way to build tree-based CLI commands and subcommands. A key design feature is that command implementations do not need to import this package, only the standard flag package.

Each command can have its own flags and nested subcommands. The package automatically handles parsing, help text generation, and execution.

Installation

go get resenje.org/command

Usage

Simple Command

package main

import (
    "flag"
    "fmt"

    "resenje.org/command"
)

func main() {
    command.Main(&command.Command{
        Help:  "A simple greeter.",
        Execute: executeGreeter,
    })
}

func executeGreeter(fs *flag.FlagSet) func(args []string) error {
    var greeting string
    fs.StringVar(&greeting, "greeting", "Hello", "The greeting to use")
    return func(args []string) error {
        if len(args) == 0 {
            fmt.Println("Please provide a name.")
            return nil
        }
        fmt.Printf("%s, %s!\n", greeting, args[0])
        return nil
    }
}

Nested Commands

package main

import (
    "flag"
    "fmt"

    "resenje.org/command"
)

func main() {
    command.Main(&command.Command{
        Help: "A git-like app",
        Subcommands: []*command.Command{
            {
                Name:  "add",
                Help:  "Add a new remote",
                Execute: executeAdd,
            },
        },
    })
}

func executeAdd(fs *flag.FlagSet) func(args []string) error {
    var dryRun bool
    fs.BoolVar(&dryRun, "n", false, "Do not actually add, just show")
    return func(args []string) error {
        if len(args) < 2 {
            fmt.Println("Error: remote name and URL required.")
            return nil
        }
        name, url := args[0], args[1]
        if dryRun {
            fmt.Printf("Would add remote %s with URL %s\n", name, url)
        } else {
            fmt.Printf("Adding remote %s with URL %s\n", name, url)
        }
        return nil
    }
}

Commands Without Flags

For commands that do not require any flags, you can use the WithoutFlags helper to simplify the Execute function.

package main

import (
    "fmt"
    "strings"

    "resenje.org/command"
)

func main() {
    command.Main(&command.Command{
        Subcommands: []*command.Command{
            &command.Command{
                Name:  "echo",
                Help:  "Prints its arguments to the screen.",
                Execute: command.WithoutFlags(echoFunc),
            },
        },
    })
}

func echoFunc(args []string) error {
    fmt.Println(strings.Join(args, " "))
    return nil
}

Running this with go run . echo hello world would output:

hello world

Middleware

You can use the WithMiddleware helper to add functionality that runs before or after a command's execution. This is useful for cross-cutting concerns like logging, metrics, or authentication.

A middleware is a function that takes the next execution function and returns a new one.

// This middleware logs a message before and after the command runs.
func loggingMiddleware(next func(args []string) error) func(args []string) error {
    return func(args []string) error {
        fmt.Println("Middleware: before execution")
        err := next(args)
        fmt.Println("Middleware: after execution")
        return err
    }
}

// In your main function, wrap the Execute function with WithMiddleware.
var rootCmd = &command.Command{
    Help: "A simple command with middleware.",
    Execute: command.WithMiddleware(
        func(fs *flag.FlagSet) func(args []string) error {
            return func(args []string) error {
                fmt.Println("Executing command")
                return nil
            }
        },
        loggingMiddleware, // Apply the middleware
    ),
}

Running this application would produce the following output, showing that the middleware wraps the core command logic:

Middleware: before execution
Executing command
Middleware: after execution

Help Text

The help text is automatically generated based on the Help fields of the Command structs.

Running the nested example with go run . remote -h will produce the following output:

USAGE

  main remote [options...] command

Manage remotes

COMMANDS

  add    Add a new remote

OPTIONS

  -h	Show help for this command.

Comparison with Other Libraries

There are several excellent CLI libraries in the Go ecosystem. This library is designed to be a simple, dependency-free solution for creating basic to moderately complex CLI tools.

Here is a brief comparison with two of the most popular libraries, spf13/cobra and urfave/cli.

Feature resenje.org/command urfave/cli spf13/cobra
Subcommands
Automatic Help
Shell Completion
Flag Parsing Standard flag Advanced Advanced (pflag)
Config Files Via plugin ✅ (with Viper)
Generator Tool
Dependencies None None Multiple
Simplicity Very High High Medium

Advantages of resenje.org/command

  • Simplicity: The API is minimal and easy to learn. If you know how to use the standard flag package, you can be productive immediately.
  • Zero Dependencies: It only uses the Go standard library. This keeps your binaries small and your dependency tree clean.
  • Decoupled Command Logic: Command implementations do not need to import resenje.org/command. The Execute function only receives a *flag.FlagSet, allowing command logic to be written and tested in complete isolation from the command runner. This promotes reusable and portable code.
  • Declarative API: The struct-based definition for commands makes the hierarchy clear and easy to read.

Disadvantages

  • Limited Features: It lacks advanced features found in other libraries, such as shell completion, configuration file integration, and command generators.
  • Basic Flag Support: It uses the standard flag package, which is less powerful than the pflag library used by Cobra (e.g., pflag offers better POSIX compatibility and more flag types).

When to Use resenje.org/command

This library is an excellent choice when you need to build a CLI with subcommands but do not require the extensive features of a library like Cobra. It is ideal for internal tools, simple daemons, or any application where you want to maintain zero dependencies and prefer a minimal API.