clir is a lightweight, composable router for building command-line interfaces in Go.
It provides:
- HTTP-style routing for CLI arguments
- Named parameters and extra trailing args
- Middleware chaining
- Nested route groups
- Typed context resolution (
WithContext,WithChildContext) - A tiny API inspired by
chi
go get github.com/yourname/go-clirpackage main
import (
"context"
"fmt"
"github.com/yourname/go-clir"
)
func main() {
r := clir.New()
r.Routes(func(b *clir.Builder) {
b.Handle("hello", "Say hello", func(req *clir.Request) error {
fmt.Println("hello world")
return nil
})
})
_ = r.Run(context.Background(), []string{"hello"})
}r := clir.New()
r.Routes(func(b *clir.Builder) {
b.Handle("comp <component> build", "Build a component", func(req *clir.Request) error {
fmt.Printf("component=%s extra=%v\n", req.Params["component"], req.Extra)
return nil
})
})
_ = r.Run(context.Background(), []string{"comp", "api", "build", "--tag", "latest"})
// component=api extra=[--tag latest]log := func(label string) clir.Middleware {
return func(next clir.Handler) clir.Handler {
return func(req *clir.Request) error {
fmt.Println("before", label)
err := next(req)
fmt.Println("after", label)
return err
}
}
}
r := clir.New()
r.Routes(func(b *clir.Builder) {
b.With(
log("outer"),
log("inner"),
).Handle("run", "Run", func(req *clir.Request) error {
fmt.Println("handler")
return nil
})
})
_ = r.Run(context.Background(), []string{"run"})
// before outer
// before inner
// handler
// after inner
// after outertype App struct{ Name string }
resolveApp := func(req *clir.Request) (App, error) {
return App{Name: "cli-app"}, nil
}
r := clir.New()
r.Routes(func(b *clir.Builder) {
app := clir.WithContext(b, resolveApp)
app.Handle("ping", "Ping the app", func(req *clir.Request, ctx App) error {
fmt.Println("app:", ctx.Name)
return nil
})
})
_ = r.Run(context.Background(), []string{"ping"})
// app: cli-apptype App struct{ Name string }
type Component struct {
App App
Name string
}
resolveApp := func(req *clir.Request) (App, error) {
return App{Name: "cli-app"}, nil
}
resolveComponent := func(app App, req *clir.Request) (Component, error) {
return Component{
App: app,
Name: req.Params["component"],
}, nil
}
r := clir.New()
r.Routes(func(b *clir.Builder) {
app := clir.WithContext(b, resolveApp)
app.Route("comp <component>", func(b *clir.ContextBuilder[App]) {
comp := clir.WithChildContext(b, resolveComponent)
comp.Route("image", func(b *clir.ContextBuilder[Component]) {
b.Handle("build", "Build images", func(req *clir.Request, c Component) error {
fmt.Printf("app=%s comp=%s\n", c.App.Name, c.Name)
return nil
})
})
})
})
_ = r.Run(context.Background(), []string{"comp", "api", "image", "build"})
// app=cli-app comp=apir := clir.New()
r.Routes(func(b *clir.Builder) {
b.Handle("hello", "Say hello", func(req *clir.Request) error {
fmt.Println("hello world")
return nil
})
})
r.PrintHelp(os.Stdout)
// Available commands:
// hello Say helloUse Handle for executable commands. Use Describe for non-executable route
metadata that should appear in contextual help.
FPrintHelp is explicit: it prints help for the argv scope you pass. It does
not parse or strip trailing help tokens. If your CLI accepts help, --help,
or another convention, parse that in your application and pass the intended
scope to FPrintHelp.
if clir.IsHelpRequest(argv) {
return r.FPrintHelp(ctx, w, clir.StripHelpToken(argv))
}
return r.Run(ctx, argv)If your application has executable routes whose final segment is help, check
for those routes before applying this convention. For example,
Handle("comp <component> help", ...) is a real command; the generic snippet
above would instead treat comp api help as a contextual help request.
r := clir.New()
r.Routes(func(b *clir.Builder) {
// Root help metadata. This route is not executable.
b.Describe("", "Example CLI commands.")
// Describe routes contribute descriptions to help, but do not run.
b.Describe("comp", "Component commands.")
b.Describe("comp <component>", "Manage one component.")
// Executable commands.
b.Handle("comp <component> status", "Show component status", func(req *clir.Request) error {
fmt.Println("status:", req.Params["component"])
return nil
})
})
_ = r.FPrintHelp(context.Background(), os.Stdout, nil, clir.Depth(1))
// Example CLI commands.
//
// Available commands:
// comp Component commands.
_ = r.FPrintHelp(context.Background(), os.Stdout, []string{"comp", "api"}, clir.Depth(1))
// Manage one component.
//
// Available commands:
// comp <component> status Show component statusUse HelpRoutes when you want clir to select scoped help routes but your
application wants to render or combine them itself:
routes := r.HelpRoutes(nil,
clir.LitDepth(1),
clir.IncludeTags("common"),
clir.ExcludeTags("debug"),
)
clir.WriteHelpInline(os.Stdout, routes)By default contextual help prints all descendants under the scope. Use Depth
or LitDepth to limit output relative to that scope. Use Where for custom
route filtering:
_ = r.FPrintHelp(context.Background(), os.Stdout, []string{"comp"}, clir.Depth(1))
_ = r.FPrintHelp(context.Background(), os.Stdout, []string{"comp"}, clir.LitDepth(1))
_ = r.FPrintHelp(context.Background(), os.Stdout, nil, clir.Where(func(route clir.RouteInfo) bool {
return route.HasTag("common")
}))Depth counts all route segments after the scope. LitDepth counts only
literal segments, so parameter segments such as <component> do not consume
depth.
Callers that relied on the previous one-level contextual help default should
pass Depth(1) explicitly.