Boa adds a declarative layer on top of github.com/spf13/cobra.
The goal is making the process of creating a command line interface as simple as possible, while still providing access to cobra primitives when needed.
Full Documentation - This README is a condensed summary. See the docs for detailed guides on enrichers, validation, lifecycle hooks, and advanced features.
go get github.com/GiGurra/boa@latest
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Foo string `descr:"a foo"`
Bar int `descr:"a bar" env:"BAR_X" optional:"true"`
Path string `positional:"true"`
Baz string `positional:"true" default:"cba"`
FB string `positional:"true" optional:"true"`
}
func main() {
boa.CmdT[Params]{
Use: "hello-world",
Short: "a generic cli tool",
Long: `A generic cli tool that has a longer description. See the README.MD for more information`,
RunFunc: func(params *Params, cmd *cobra.Command, args []string) {
fmt.Printf(
"Hello world with params: %s, %d, %s, %s, %s\n",
params.Foo, // string (access directly)
params.Bar, // int (access directly)
params.Path, // string
params.Baz, // string
params.FB, // string
)
},
}.Run()
}Help output for the above:
A generic cli tool that has a longer description. See the README.MD for more information
Usage:
hello-world <path> <baz> [f-b] [flags]
Flags:
--bar int a bar (env: BAR_X) (default 4)
-f, --foo string a foo (env: FOO, required)
-h, --help help for hello-world
| Tag | Description | Example |
|---|---|---|
descr / desc / description / help |
Description text for help | descr:"User name" |
name / long |
Override flag name | name:"user-name" |
default |
Default value | default:"8080" |
env |
Environment variable name | env:"PORT" |
short |
Short flag (single char) | short:"p" |
positional / pos |
Marks positional argument | positional:"true" |
required / req |
Marks as required | required:"true" |
optional / opt |
Marks as optional | optional:"true" |
alts / alternatives |
Allowed values (enum) | alts:"debug,info,warn,error" |
strict-alts / strict |
Validate against alts | strict:"true" |
For advanced programmatic configuration (setting defaults, alternatives, conditional requirements), see the Context-Aware Hooks section.
The ParamEnrich field controls parameter enrichment:
| Value | Behavior |
|---|---|
nil |
Uses ParamEnricherDefault (enriches everything including env vars) |
ParamEnricherDefault |
Explicit default: derives names, short flags, env vars, and bool defaults |
ParamEnricherNone |
No enrichment - you must specify everything via struct tags |
ParamEnricherDefault includes:
| Enricher | Behavior |
|---|---|
ParamEnricherName |
Converts field name to kebab-case flag (e.g., MyParam → --my-param) |
ParamEnricherShort |
Auto-assigns short flag from first character (skips h for help, avoids conflicts) |
ParamEnricherEnv |
Generates env var from flag name (e.g., --my-param → MY_PARAM) |
ParamEnricherBool |
Sets default false for boolean params without explicit defaults |
Consider composing your own enricher if you don't want auto-generated env vars for every parameter:
// Custom enricher without auto env vars
boa.NewCmdT[Params]("cmd").WithParamEnrich(
boa.ParamEnricherCombine(
boa.ParamEnricherName,
boa.ParamEnricherShort,
boa.ParamEnricherBool,
),
)
// Or with prefixed env vars
boa.NewCmdT[Params]("cmd").WithParamEnrich(
boa.ParamEnricherCombine(
boa.ParamEnricherName,
boa.ParamEnricherEnv,
boa.ParamEnricherEnvPrefix("MYAPP"), // MY_PARAM → MYAPP_MY_PARAM
),
)Create hierarchical CLI tools with sub-commands:
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type SubParams struct {
Foo string `descr:"a foo"`
Bar int `descr:"a bar" env:"BAR_X" default:"4"`
Path string `positional:"true"`
Baz string `positional:"true" default:"cba"`
FB string `positional:"true" optional:"true"`
}
type OtherParams struct {
Foo2 string `descr:"a foo"`
}
func main() {
boa.NewCmdT[boa.NoParams]("hello-world").
WithShort("a generic cli tool").
WithLong("A generic cli tool that has a longer description").
WithSubCmds(
boa.NewCmdT[SubParams]("subcommand1").
WithShort("a subcommand").
WithRunFunc(func(params *SubParams) {
fmt.Printf("Hello world from subcommand1 with params: %s, %d, %s, %s\n",
params.Foo, params.Bar, params.Path, params.Baz)
}),
boa.NewCmdT[OtherParams]("subcommand2").
WithShort("a subcommand").
WithRunFunc(func(params *OtherParams) {
fmt.Println("Hello world from subcommand2")
}),
).
Run()
}Help output for the above:
a subcommand
Usage:
hello-world subcommand1 <path> <baz> [f-b] [flags]
Flags:
--bar int a bar (env: BAR_X) (default 4)
-f, --foo string a foo (env: FOO, required)
-h, --help help for subcommand1
You can compose structs to create more complex parameter structures:
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"time"
)
type Base1 struct {
Foo string
Bar int
File string
}
type Base2 struct {
Foo2 string
Bar2 int
File2 string
}
type Combined struct {
Base Base1
Base2
Baz string
FB string `optional:"true"`
Time time.Time `optional:"true"`
}
func main() {
boa.NewCmdT[Combined]("hello-world").
WithShort("a generic cli tool").
WithLong("A generic cli tool that has a longer description").
WithRunFunc(func(params *Combined) {
fmt.Printf(
"Hello world with params: %s, %d, %s, %s, %s, %v\n",
params.Base.Foo, // string
params.Base.Bar, // int
params.Base.File, // string
params.Baz, // string
params.FB, // string
params.Time, // time.Time
)
}).
Run()
}Note: Nested struct fields use their own field names as flags, not prefixed with the parent struct name.
For example, Base.Foo becomes --foo, not --base-foo. See "Missing features" for planned prefix support.
Access the underlying Cobra command for advanced customization:
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Baz string
FB string
}
func main() {
boa.NewCmdT[Params]("hello-world").
WithShort("a generic cli tool").
WithLong("A generic cli tool that has a longer description").
WithInitFunc2E(func(params *Params, cmd *cobra.Command) error {
cmd.Deprecated = "this command is deprecated"
return nil
}).
WithRunFunc(func(params *Params) {
fmt.Printf("Hello world with params: %s, %s\n",
params.Baz,
params.FB,
)
}).
Run()
}You can make parameters conditionally required or enabled using HookContext:
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Mode string // when "file", FilePath is required
FilePath string `optional:"true"`
Verbose bool `optional:"true"` // only enabled when Debug is true
Debug bool `optional:"true"`
}
func main() {
boa.NewCmdT[Params]("hello-world").
WithShort("a generic cli tool").
WithInitFuncCtx(func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
// FilePath is required when Mode is "file"
ctx.GetParam(&p.FilePath).SetRequiredFn(func() bool {
return p.Mode == "file"
})
// Verbose is only enabled when Debug is true
ctx.GetParam(&p.Verbose).SetIsEnabledFn(func() bool {
return p.Debug
})
return nil
}).
WithRunFunc(func(params *Params) {
fmt.Printf("Hello World! Mode=%s\n", params.Mode)
}).
Run()
}You can specify that a parameter must be one of a set of values using the alts tag:
type Params struct {
LogLevel string `alts:"debug,info,warn,error" strict:"true"`
Format string `alts:"json,yaml,toml"` // suggestions only (strict defaults to true)
}Boa supports array/slice types with proper parsing:
type Params struct {
Numbers []int `descr:"list of numbers"`
Tags []string `descr:"tags" default:"[a,b,c]"`
Ports []int64 `descr:"ports" default:"[8080,8081,8082]"`
}A structured builder API is available for more complex command creation:
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
)
type Params struct {
Flag1 string
Flag2 int
}
func main() {
cmd := boa.NewCmdT[Params]("my-command").
WithShort("A command description").
WithLong("A longer command description").
WithRunFunc(func(params *Params) {
fmt.Printf("Running with: %s, %d\n",
params.Flag1,
params.Flag2,
)
}).
WithSubCmds(
boa.NewCmdT[Params]("subcommand1"),
//...etc
)
cmd.Run()
}package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type AppConfig struct {
Host string
Port int
}
type ConfigFromFile struct {
File string `descr:"config file path"`
AppConfig
}
func main() {
boa.NewCmdT[ConfigFromFile]("my-app").
WithPreValidateFuncCtx(func(ctx *boa.HookContext, params *ConfigFromFile, cmd *cobra.Command, args []string) error {
// Load configuration from file if provided
// boa.UnMarshalFromFileParam is a helper to unmarshal from a file
// CLI and env var values take precedence over file values
fileParam := ctx.GetParam(¶ms.File)
return boa.UnMarshalFromFileParam(fileParam, ¶ms.AppConfig, nil)
}).
WithRunFunc(func(params *ConfigFromFile) {
// Use parameters loaded from the file
fmt.Printf("Host: %s, Port: %d\n",
params.Host,
params.Port,
)
}).
Run()
}Boa supports multiple sources for parameter values, including command-line flags, environment variables, and config files. When multiple sources are available, the following priority order is used:
- Command-line flags: Values provided directly on the command line take precedence over all other sources.
- Environment variables: If a command-line flag is not provided, the corresponding environment variable will be used if it exists.
- Config files: If neither a command-line flag nor an environment variable is provided, the value from the configuration file will be used.
- Default values: If no value is provided from any source, the default value specified in the parameter definition will be used.
- Zero value: If no value is provided from any source and no default value is specified, the zero value for the parameter type will be used.
Boa provides several lifecycle hooks that can be implemented or defined to customize behavior at different stages of command execution. These hooks give you fine-grained control over parameter initialization, validation, and execution.
The Init hook runs during the initialization phase, after boa creates internal parameter mirrors but before cobra
flags are registered. This allows you to configure parameters (set defaults, env vars, validators) via HookContext
before they become CLI flags.
package main
import (
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
// Implement this interface on your configuration struct
type CfgStructInit interface {
Init() error
}
// Example implementation
func (i *MyConfigStruct) Init() error {
// Initialize defaults, set up validators, etc.
i.SomeParam.Default = boa.Default("default value")
return nil
}
// Alternatively, use the InitFunc in Cmd
func main() {
boa.Cmd{
Params: ¶ms,
InitFunc: func(params any) error {
// Custom initialization logic
return nil
},
}.Run()
// Or with the builder API
boa.NewCmdT[MyConfigStruct]("command").
WithInitFuncE(func(params *MyConfigStruct) error {
// Custom initialization logic
return nil
})
}The PostCreate hook runs after cobra flags have been created but before any command-line arguments are parsed. This is useful when you need to inspect or modify the cobra command after flags are registered.
package main
import (
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
func main() {
boa.NewCmdT[MyConfigStruct]("command").
WithPostCreateFuncCtx(func(ctx *boa.HookContext, params *MyConfigStruct, cmd *cobra.Command) error {
// Cobra flags are now available
flag := cmd.Flags().Lookup("my-flag")
if flag != nil {
// Inspect or modify flag properties
}
return nil
})
}The PreValidate hook runs after parameters are parsed from the command line and environment variables but before validation is performed.
package main
import (
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
// Implement this interface on your configuration struct
type CfgStructPreValidate interface {
PreValidate() error
}
// Example implementation
func (i *MyConfigStruct) PreValidate() error {
// Manipulate parameters before validation
return nil
}
// Alternatively, use the PreValidateFunc in Cmd
func main() {
boa.Cmd{
Params: ¶ms,
PreValidateFunc: func(params any, cmd *cobra.Command, args []string) error {
// Custom pre-validation logic
return nil
},
}.Run()
// Or with the builder API
boa.NewCmdT[MyConfigStruct]("command").
WithPreValidateFuncE(func(params *MyConfigStruct, cmd *cobra.Command, args []string) error {
// Custom pre-validation logic, such as loading from config files
return nil
})
}The PreExecute hook runs after parameter validation but before the command's Run function is executed.
package main
import (
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
// Implement this interface on your configuration struct
type CfgStructPreExecute interface {
PreExecute() error
}
// Example implementation
func (i *MyConfigStruct) PreExecute() error {
// Setup that should happen after validation but before execution
return nil
}
// Alternatively, use the PreExecuteFunc in Cmd
func main() {
boa.Cmd{
Params: ¶ms,
PreExecuteFunc: func(params any, cmd *cobra.Command, args []string) error {
// Custom pre-execution logic
return nil
},
}.Run()
// Or with the builder API
boa.NewCmdT[MyConfigStruct]("command").
WithPreExecuteFuncE(func(params *MyConfigStruct, cmd *cobra.Command, args []string) error {
// Custom pre-execution logic
return nil
})
}Hooks are executed in the following order:
- Init - Parameter mirrors exist, cobra flags not yet created (configure params here)
- PostCreate - Cobra flags are now registered (inspect/modify flags here)
- PreValidate - After flags are parsed but before validation
- Validation - Built-in parameter validation
- PreExecute - After validation but before command execution
- Run - The actual command execution
- Init: Set up default values, configure custom validators
- PostCreate: Inspect or modify cobra flags after they're registered
- PreValidate: Load configurations from files, set derived parameters
- PreExecute: Establish connections, prepare resources needed for execution
All hooks can return errors to abort command execution. If any hook returns an error, the command will not proceed to the next phase, and the error will be reported to the user.
For advanced use cases, boa provides context-aware hooks that give access to the underlying parameter mirrors.
The HookContext provides:
GetParam(fieldPtr any) Param- Get the Param interface for any fieldHasValue(fieldPtr any) bool- Check if a parameter has a value from any source (CLI, env, default, or injection)AllMirrors() []Param- Get all auto-generated parameter mirrors
package main
import (
"github.com/GiGurra/boa/pkg/boa"
)
type ServerConfig struct {
Host string
Port int
LogLevel string
}
// InitCtx is called during initialization with HookContext access
func (c *ServerConfig) InitCtx(ctx *boa.HookContext) error {
// Configure the Host parameter
hostParam := ctx.GetParam(&c.Host)
hostParam.SetDefault(boa.Default("localhost"))
hostParam.SetEnv("SERVER_HOST")
// Configure the Port parameter
portParam := ctx.GetParam(&c.Port)
portParam.SetDefault(boa.Default(8080))
portParam.SetEnv("SERVER_PORT")
// Set up alternatives with shell completion for LogLevel
logParam := ctx.GetParam(&c.LogLevel)
logParam.SetDefault(boa.Default("info"))
logParam.SetAlternatives([]string{"debug", "info", "warn", "error"})
logParam.SetStrictAlts(true) // Validation fails if value not in list
return nil
}
func main() {
boa.NewCmdT[ServerConfig]("server").
WithRunFunc(func(params *ServerConfig) {
// Use params.Host, params.Port, params.LogLevel
}).
Run()
}Available context-aware interfaces:
CfgStructInitCtx-InitCtx(ctx *HookContext) errorCfgStructPreValidateCtx-PreValidateCtx(ctx *HookContext) errorCfgStructPreExecuteCtx-PreExecuteCtx(ctx *HookContext) error
package main
import (
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Config struct {
Name string
Verbose bool
}
func main() {
boa.NewCmdT[Config]("app").
WithInitFuncCtx(func(ctx *boa.HookContext, params *Config, cmd *cobra.Command) error {
// Configure parameters programmatically
nameParam := ctx.GetParam(¶ms.Name)
nameParam.SetDefault(boa.Default("default-name"))
nameParam.SetShort("n")
nameParam.SetAlternatives([]string{"alice", "bob", "carol"})
return nil
}).
WithRunFunc(func(params *Config) {
// Use params
}).
Run()
}Available function-based context hooks:
WithInitFuncCtx- During initializationWithPostCreateFuncCtx- After cobra flags are createdWithPreValidateFuncCtx- After parsing, before validationWithPreExecuteFuncCtx- After validation, before executionWithRunFuncCtx/WithRunFuncCtx4- Command execution with HookContext access
Use WithRunFuncCtx when you need to check whether optional parameters actually have a value, even if that explicitly is the go default zero value:
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
)
type Params struct {
Host string `default:"localhost"`
Port int `optional:"true"`
}
func main() {
boa.NewCmdT[Params]("server").
WithRunFuncCtx(func(ctx *boa.HookContext, params *Params) {
// Check if parameters have values (from CLI, env, default, or injection)
if ctx.HasValue(¶ms.Port) {
fmt.Printf("Starting server on %s:%d\n", params.Host, params.Port)
} else {
fmt.Printf("Starting server on %s (no port specified)\n", params.Host)
}
}).
Run()
}Note: You cannot use both WithRunFunc and WithRunFuncCtx on the same command - choose one or the other.
Boa provides error-returning variants of the run functions for better error handling in your commands:
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
)
type Params struct {
File string
}
func main() {
err := boa.NewCmdT[Params]("process").
WithRunFuncE(func(params *Params) error {
if params.File == "" {
return fmt.Errorf("file cannot be empty")
}
// Process file...
return nil
}).
RunE() // Returns error instead of calling os.Exit
if err != nil {
fmt.Printf("Command failed: %v\n", err)
}
}| Builder Method | Signature |
|---|---|
WithRunFuncE |
func(params *T) error |
WithRunFuncE3 |
func(params *T, cmd *cobra.Command, args []string) error |
WithRunFuncCtxE |
func(ctx *HookContext, params *T) error |
WithRunFuncCtxE4 |
func(ctx *HookContext, params *T, cmd *cobra.Command, args []string) error |
| Method | Description |
|---|---|
Run() |
Executes command, panics on any error |
RunE() |
Executes command, returns error |
RunArgs(args) |
Executes with args, panics on any error |
RunArgsE(args) |
Executes with args, returns error |
ToCobra() |
Returns *cobra.Command with cmd.Run set (panics on setup error) |
ToCobraE() |
Returns (*cobra.Command, error) with cmd.RunE set |
The two execution methods handle errors differently:
Run()- All errors (hook errors, runtime errors, config errors) cause panicsRunE()- All errors are returned for programmatic handling
// With Run() - errors panic
boa.NewCmdT[Params]("cmd").
WithRunFuncE(func(p *Params) error {
return fmt.Errorf("something went wrong")
}).
Run() // Panics with the error
// With RunE() - errors are returned
err := boa.NewCmdT[Params]("cmd").
WithRunFuncE(func(p *Params) error {
return fmt.Errorf("something went wrong")
}).
RunE() // err contains "something went wrong"
if err != nil {
// Handle error programmatically
}This includes all error types:
- Hook errors (
InitFunc,PreValidate,PreExecute) - Runtime errors from
RunFuncE - Configuration errors (e.g., setting multiple run functions)
- Use
RunFunc/Run()for simple CLIs where errors should terminate the program - Use
RunFuncE/RunE()when you need to:- Handle errors programmatically
- Write testable command logic
- Integrate with frameworks that expect error returns
- Build commands that are called from other Go code
If you're migrating from the deprecated Required[T]/Optional[T] wrapper types:
type Params struct {
Name boa.Required[string] `descr:"User name"`
Port boa.Optional[int] `descr:"Port number" default:"8080"`
}
// Accessing values
fmt.Println(params.Name.Value()) // string
fmt.Println(*params.Port.Value()) // int (via pointer)type Params struct {
Name string `descr:"User name"` // required by default
Port int `descr:"Port number" optional:"true"`
}
// Accessing values - direct access
fmt.Println(params.Name) // string
fmt.Println(params.Port) // int (direct value)For programmatic configuration that was previously done directly on wrapper types:
Before:
params.Port.SetRequiredFn(func() bool { return params.Mode == "server" })After:
// Use HookContext in InitFuncCtx
cmd := boa.NewCmdT[Params]("app").
WithInitFuncCtx(func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
ctx.GetParam(&p.Port).SetRequiredFn(func() bool { return p.Mode == "server" })
return nil
})The Required[T] and Optional[T] wrapper types are deprecated but still functional for backward compatibility.
// DEPRECATED - prefer plain Go types instead
type Params struct {
Name boa.Required[string] // Use: Name string
Port boa.Optional[int] // Use: Port int `optional:"true"`
}
// DEPRECATED factory functions
name := boa.Req("default") // Use: struct tag `default:"default"`
port := boa.Opt(8080) // Use: struct tag `default:"8080" optional:"true"`
def := boa.Default(value) // Use: struct tag `default:"value"`The wrapper types require calling .Value() to access values, which adds verbosity compared to direct field access.
- Support for custom types
- Prefixed nested config
- Stable API with plain Go types as the primary interface
- Used in production projects