Skip to content

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-clir

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

Install

go get github.com/yourname/go-clir

Basic Example

package 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"})
}

Routing With Parameters & Extra Arguments

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]

Middleware

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 outer

Typed Contexts

Single Layer

type 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-app

Layered Contexts (WithChildContext)

type 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=api

Printing Help

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.PrintHelp(os.Stdout)

// Available commands:
//   hello   Say hello

Contextual Help and Describe Metadata

Use 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 status

Use 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.

About

A simple Go CLI router

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages