Awesome Go

boa

CategoryCommand Line
SubcategoryStandard CLI
Stars0

Declarative flags, env vars, validation, and config files from struct tags. Built on cobra

About boa

BOA

CI Status Go Report Card

Like if kong and urfave/cli had a baby and made it cobra compatible.

Self-documenting CLIs from Go structs. Define your parameters once and get flags, env vars, validation, config file loading, and help text — all generated automatically. The result is a CLI that's easy to write, easy for humans to use, and easy for LLMs to invoke — because the full parameter schema is right there in --help.

Built on top of cobra, not replacing it. Full cobra interop when you need it.

Full Documentation

Quick Start

go get github.com/GiGurra/boa@latest
type Params struct {
    Name string `descr:"your name"`
    Port int    `descr:"port number" default:"8080" optional:"true"`
}

func main() {
    boa.CmdT[Params]{
        Use:   "my-app",
        Short: "a simple CLI tool",
        RunFunc: func(params *Params, cmd *cobra.Command, args []string) {
            fmt.Printf("Hello %s on port %d\n", params.Name, params.Port)
        },
    }.Run()
}

This is what you get — flag names, short flags, defaults, required/optional, descriptions, and usage line all generated from the struct:

$ my-app --help
a simple CLI tool

Usage:
  my-app [flags]

Flags:
  -h, --help          help for my-app
  -n, --name string   your name (required)
  -p, --port int      port number (default 8080)

And this is how you interact with it:

$ my-app --name Alice
Hello Alice on port 8080

$ my-app --name Bob --port 3000
Hello Bob on port 3000

$ my-app
Usage:
  my-app [flags]

Flags:
  -h, --help          help for my-app
  -n, --name string   your name (required)
  -p, --port int      port number (default 8080)

Error: required flag "name" not set

All standard Go types work out of the box:

type Params struct {
    Host    string            `descr:"server host"`                    // required by default
    Port    int               `descr:"port" default:"8080"`            // with default
    Name    *string           `descr:"user name"`                      // pointer = optional, nil = not set
    Tags    []string          `descr:"tags" default:"[a,b,c]"`         // --tags a,b,c
    Labels  map[string]string `descr:"labels"`                         // --labels env=prod,team=backend
    Input   string            `positional:"true"`                      // positional arg
    Timeout time.Duration     `descr:"timeout" default:"30s"`          // durations, IPs, URLs, etc.
    Matrix  [][]int           `descr:"matrix" optional:"true"`         // complex types use JSON: '[[1,2],[3,4]]'
}

Pointer fields are optional by default — nil means "not set", so you can distinguish between "user passed zero" and "user didn't pass anything":

type Params struct {
    Retries *int `descr:"retry count"` // nil if not provided, *0 if --retries 0
}

boa.CmdT[Params]{
    Use: "app",
    RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
        if p.Retries != nil {
            fmt.Printf("Retrying %d times\n", *p.Retries)
        } else {
            fmt.Println("Using default retry strategy")
        }
    },
}.Run()
type ServeParams struct {
    Host string `descr:"bind address" default:"0.0.0.0"`
    Port int    `descr:"port" default:"8080"`
}

type DeployParams struct {
    Target string `descr:"deploy target" alts:"staging,production" strict:"true"`
    DryRun bool   `descr:"dry run mode" optional:"true"`
}

func main() {
    boa.CmdT[boa.NoParams]{
        Use:   "my-app",
        Short: "a multi-command CLI",
        SubCmds: boa.SubCmds(
            boa.CmdT[ServeParams]{
                Use: "serve", Short: "start the server",
                RunFunc: func(p *ServeParams, cmd *cobra.Command, args []string) {
                    fmt.Printf("Serving on %s:%d\n", p.Host, p.Port)
                },
            },
            boa.CmdT[DeployParams]{
                Use: "deploy", Short: "deploy the app",
                RunFunc: func(p *DeployParams, cmd *cobra.Command, args []string) {
                    fmt.Printf("Deploying to %s (dry-run: %v)\n", p.Target, p.DryRun)
                },
            },
        ),
    }.Run()
}
$ my-app --help
a multi-command CLI

Usage:
  my-app [command]

Available Commands:
  serve       start the server
  deploy      deploy the app

$ my-app deploy --target staging --dry-run
Deploying to staging (dry-run: true)

$ my-app bogus
Error: unknown command "bogus" for "my-app"

Tag a field with configfile and boa loads it automatically. CLI and env vars always win:

type Params struct {
    ConfigFile string     `configfile:"true" optional:"true" default:"config.json"`
    Host       string     `descr:"server host"`
    Port       int        `descr:"port" default:"8080"`
    Internal   [][]string `boa:"configonly"` // loaded from config only, no CLI flag
}
$ cat config.json
{"Host": "prod.example.com", "Port": 443, "Internal": [["a","b"],["c","d"]]}

$ my-app                              # uses config.json values
$ my-app --host override.local        # CLI wins over config file
$ my-app --config-file staging.json   # different config file
$ HOST=ci.local my-app                # env var wins over config file

Nested structs can have their own config files. Priority: CLI > env > root config > substruct config > defaults:

type DBConfig struct {
    ConfigFile string `configfile:"true" optional:"true"`
    Host       string `default:"localhost"`
    Port       int    `default:"5432"`
}

type Params struct {
    ConfigFile string   `configfile:"true" optional:"true" default:"app.json"`
    DB         DBConfig
}

JSON is built in. Register other formats with one line:

boa.RegisterConfigFormat(".yaml", yaml.Unmarshal)
boa.RegisterConfigFormat(".toml", toml.Unmarshal)

Named fields auto-prefix their children. Embedded fields stay flat:

type DBConfig struct {
    Host string `default:"localhost"`
    Port int    `default:"5432"`
}

type CommonFlags struct {
    Verbose bool `optional:"true"`
}

type Params struct {
    CommonFlags           // embedded: --verbose (no prefix)
    Primary DBConfig      // named: --primary-host, --primary-port
    Replica DBConfig      // named: --replica-host, --replica-port
}
$ my-app --help
Flags:
  --verbose              (default false)
  --primary-host string  (default "localhost")
  --primary-port int     (default 5432)
  --replica-host string  (default "localhost")
  --replica-port int     (default 5432)

Deep nesting chains prefixes: Infra.Primary.Host becomes --infra-primary-host. Env vars follow the same pattern: INFRA_PRIMARY_HOST.

Struct tag validation:

type Params struct {
    Port     int    `descr:"port" min:"1" max:"65535"`
    LogLevel string `descr:"log level" alts:"debug,info,warn,error" strict:"true"`
    Name     string `descr:"name" pattern:"^[a-z]+$"`
    Tags     []string `descr:"tags" min:"1" max:"5"` // min/max = slice length
}

Programmatic validation with HookContext — for cases where struct tags aren't enough:

type Params struct {
    Host string `descr:"Server hostname"`
    Port int    `descr:"Server port"`
    CIDR string `descr:"Allowed CIDR range" optional:"true"`
}

boa.CmdT[Params]{
    Use: "server",
    InitFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
        // Type-safe custom validator
        boa.GetParamT(ctx, &p.Port).SetCustomValidatorT(func(port int) error {
            if port < 1024 && port != 80 && port != 443 {
                return fmt.Errorf("non-standard privileged port %d", port)
            }
            return nil
        })

        // Conditional required: CIDR only required when host is not localhost
        ctx.GetParam(&p.CIDR).SetRequiredFn(func() bool {
            return p.Host != "localhost"
        })

        return nil
    },
    RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
        fmt.Printf("Listening on %s:%d\n", p.Host, p.Port)
    },
}.Run()

You can also implement validation directly on your params struct:

type ServerConfig struct {
    Host     string
    Port     int
    LogLevel string
}

func (c *ServerConfig) InitCtx(ctx *boa.HookContext) error {
    ctx.GetParam(&c.Port).SetDefault(boa.Default(8080))
    ctx.GetParam(&c.LogLevel).SetAlternatives([]string{"debug", "info", "warn", "error"})
    ctx.GetParam(&c.LogLevel).SetStrictAlts(true)
    return nil
}
MethodBehavior
Run()Shows usage + error on bad input, exits 1. Other errors panic.
RunE()Returns all errors silently for programmatic use
ToCobra()Returns *cobra.Command for custom execution via boa.Execute(cmd)

Run() for simple CLIs:

boa.CmdT[Params]{
    Use: "app",
    RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
        fmt.Println("Success!")
    },
}.Run()
// Bad input → prints usage + error, exits 1
// Runtime panic → crashes with stack trace

RunE() when you need error handling:

err := boa.CmdT[Params]{
    Use: "app",
    RunFuncE: func(p *Params, cmd *cobra.Command, args []string) error {
        if p.Port < 1024 {
            return fmt.Errorf("port must be >= 1024")
        }
        return nil
    },
}.RunE()

if err != nil {
    log.Fatalf("Command failed: %v", err)
}

ToCobra() when embedding boa in a larger cobra app:

cmd := boa.CmdT[Params]{
    Use: "sub",
    RunFunc: func(p *Params, cmd *cobra.Command, args []string) { ... },
}.ToCobra()

rootCmd.AddCommand(cmd)
boa.Execute(rootCmd) // prints usage + error on failure

Every boa CLI gets shell completions for free via cobra. No extra code needed:

$ my-app completion bash   # bash completions
$ my-app completion zsh    # zsh completions
$ my-app completion fish   # fish completions
$ my-app completion powershell  # powershell completions

Install once and get tab completion for all flags, subcommands, and enum values:

# bash
my-app completion bash > /etc/bash_completion.d/my-app

# zsh
my-app completion zsh > "${fpath[1]}/_my-app"

# fish
my-app completion fish > ~/.config/fish/completions/my-app.fish

Fields with alts automatically complete to their allowed values.

Static completions from struct tags:

type Params struct {
    LogLevel string `descr:"log level" alts:"debug,info,warn,error"` // tab-completes to these values
}

Dynamic completions — e.g. completing based on output from other CLIs:

boa.CmdT[Params]{
    Use: "deploy",
    InitFunc: func(p *Params, cmd *cobra.Command) error {
        cmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
            // Call kubectl to get real namespace list
            out, err := exec.Command("kubectl", "get", "namespaces", "-o", "jsonpath={.items[*].metadata.name}").Output()
            if err != nil {
                return nil, cobra.ShellCompDirectiveError
            }
            return strings.Fields(string(out)), cobra.ShellCompDirectiveNoFileComp
        })
        return nil
    },
    RunFunc: func(p *Params, cmd *cobra.Command, args []string) { ... },
}.Run()
// $ deploy --namespace <TAB>
// default    kube-system    production    staging
TagDescriptionExample
descr / descDescription textdescr:"User name"
name / longOverride flag namename:"user-name"
defaultDefault valuedefault:"8080"
envEnvironment variable nameenv:"PORT"
shortShort flag (single char)short:"p"
positional / posMarks positional argumentpositional:"true"
required / reqMarks as requiredrequired:"true"
optional / optMarks as optionaloptional:"true"
altsAllowed values (enum)alts:"debug,info,warn,error"
strictValidate against altsstrict:"true"
minMin value or min lengthmin:"1"
maxMax value or max lengthmax:"65535"
patternRegex patternpattern:"^[a-z]+$"
configfileAuto-load config from pathconfigfile:"true"
boaSpecial directivesboa:"ignore", boa:"configonly"

Further Reading

  • Getting Started — all parameter types, subcommands, config files
  • Struct Tags — complete tag reference with auto-prefixing
  • Validation — required/optional, alternatives, conditional requirements
  • Lifecycle Hooks — customize behavior at each stage
  • Enrichers — auto-derive flag names, env vars, short flags
  • Error Handling — Run() vs RunE() and error propagation
  • Advanced — custom types, config format registry, viper-like discovery
  • Cobra Interop — access cobra primitives, migrate incrementally

Frequently Asked Questions

What is boa?

boa is a Command Line library for the Go programming language. Declarative flags, env vars, validation, and config files from struct tags. Built on cobra

How do I install boa?

Install boa with the Go module system using `go get GiGurra/boa`. Check the repository for the current installation instructions.

What category does boa belong to?

boa is listed under Command Line, specifically Standard CLI.

← Back to Standard CLI