Advanced Features¶
This page covers advanced BOA features for power users.
The Param Interface¶
Every parameter (whether using struct tags or programmatic configuration) implements the Param interface. Access it via HookContext.GetParam():
boa.CmdT[Params]{
Use: "cmd",
InitFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
param := ctx.GetParam(&p.SomeField)
// Now use param methods...
return nil
},
}
Param Methods¶
| Method | Description |
|---|---|
SetName(string) |
Override flag name |
SetShort(string) |
Set short flag |
SetEnv(string) |
Set environment variable |
SetDefault(any) |
Set default value |
SetAlternatives([]string) |
Set allowed values |
SetAlternativesFunc(func(...) []string) |
Set dynamic completion function |
SetStrictAlts(bool) |
Enable/disable strict validation |
SetRequiredFn(func() bool) |
Dynamic required condition |
SetIsEnabledFn(func() bool) |
Dynamic visibility |
GetName() string |
Get current flag name |
GetShort() string |
Get current short flag |
GetEnv() string |
Get current env var |
GetAlternatives() []string |
Get allowed values |
GetAlternativesFunc() |
Get dynamic completion function |
HasValue() bool |
Check if value was set |
IsRequired() bool |
Check if required |
IsEnabled() bool |
Check if visible |
Typed Parameter API (ParamT)¶
For type-safe parameter configuration, use boa.GetParamT[T]() instead of GetParam(). This returns a ParamT[T] interface with typed methods:
type Params struct {
Port int `descr:"Server port"`
Host string `descr:"Server host"`
}
boa.CmdT[Params]{
Use: "server",
InitFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
// Type-safe: compiler ensures correct types
portParam := boa.GetParamT(ctx, &p.Port)
portParam.SetDefaultT(8080) // Takes int, not any
portParam.SetCustomValidatorT(func(port int) error {
if port < 1 || port > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
return nil
})
hostParam := boa.GetParamT(ctx, &p.Host)
hostParam.SetDefaultT("localhost") // Takes string
hostParam.SetAlternatives([]string{"localhost", "0.0.0.0"})
return nil
},
}
ParamT Methods¶
The ParamT[T] interface provides typed methods plus all pass-through methods from Param:
| Typed Methods | Description |
|---|---|
SetDefaultT(T) |
Set default value with compile-time type checking |
SetCustomValidatorT(func(T) error) |
Set validation function that receives the typed value |
| Pass-through Methods | Description |
|---|---|
Param() |
Access the underlying untyped Param interface |
SetAlternatives([]string) |
Set allowed values |
SetStrictAlts(bool) |
Enable/disable strict validation |
SetAlternativesFunc(...) |
Set dynamic completion function |
SetEnv(string) |
Set environment variable |
SetShort(string) |
Set short flag |
SetName(string) |
Set flag name |
SetIsEnabledFn(func() bool) |
Dynamic visibility |
SetRequiredFn(func() bool) |
Dynamic required condition |
Conditional Requirements with ParamT¶
type DeployParams struct {
Environment string `descr:"Target environment" default:"dev"`
ProdKey string `descr:"Production API key" optional:"true"`
}
boa.CmdT[DeployParams]{
Use: "deploy",
InitFuncCtx: func(ctx *boa.HookContext, p *DeployParams, cmd *cobra.Command) error {
// ProdKey is only required when deploying to production
prodKeyParam := boa.GetParamT(ctx, &p.ProdKey)
prodKeyParam.SetRequiredFn(func() bool {
return p.Environment == "prod"
})
return nil
},
}
Dynamic Shell Completion¶
AlternativesFunc¶
For completion suggestions that depend on runtime state (like fetching from an API), use SetAlternativesFunc via HookContext:
type Params struct {
Region string `descr:"AWS region"`
}
func main() {
boa.CmdT[Params]{
Use: "app",
InitFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
ctx.GetParam(&p.Region).SetAlternativesFunc(
func(cmd *cobra.Command, args []string, toComplete string) []string {
// Could fetch from API, read from file, etc.
return []string{"us-east-1", "us-west-2", "eu-west-1"}
},
)
return nil
},
RunFunc: func(params *Params, cmd *cobra.Command, args []string) {
// ...
},
}.Run()
}
ValidArgsFunc¶
For positional argument completion:
boa.CmdT[Params]{
Use: "app",
ValidArgsFunc: func(p *Params, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Return suggestions for positional args
return []string{"option1", "option2"}, cobra.ShellCompDirectiveDefault
},
}
Config File Loading¶
Using the configfile Tag¶
Tag a string field with configfile:"true" to automatically load a config file before validation:
type Params struct {
ConfigFile string `configfile:"true" optional:"true" default:"config.json"`
Host string
Port int
}
boa.CmdT[Params]{
Use: "app",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Host: %s, Port: %d\n", p.Host, p.Port)
},
}.Run()
CLI and env var values always take precedence over config file values.
Config Format Registry¶
JSON is the only format shipped by default. Register additional formats by file extension:
import "gopkg.in/yaml.v3"
boa.RegisterConfigFormat(".yaml", yaml.Unmarshal)
boa.RegisterConfigFormat(".toml", toml.Unmarshal)
Resolution order for unmarshal function:
- Explicit
ConfigUnmarshalon the command - Registered format matched by file extension
json.Unmarshal(default fallback)
You can also set ConfigUnmarshal directly on a command to override all format detection for that command:
boa.CmdT[Params]{
Use: "app",
ConfigUnmarshal: yaml.Unmarshal,
RunFunc: func(p *Params, cmd *cobra.Command, args []string) { ... },
}.Run()
Substruct Config Files¶
The configfile:"true" tag works on fields inside nested structs, not just at root. Each substruct can have its own config file:
type DBConfig struct {
ConfigFile string `configfile:"true" optional:"true"`
Host string `descr:"host" default:"localhost"`
Port int `descr:"port" default:"5432"`
}
type CacheConfig struct {
ConfigFile string `configfile:"true" optional:"true"`
TTL int `descr:"cache TTL" default:"300"`
}
type Params struct {
ConfigFile string `configfile:"true" optional:"true" default:"config.json"`
DB DBConfig
Cache CacheConfig
}
Substruct configs load first, then root config loads and overrides any overlapping values. The full merge priority is:
- CLI flags -- highest
- Environment variables
- Root config file
- Substruct config files
- Default values
- Zero value -- lowest
This lets you split configuration across multiple files while maintaining a clear override hierarchy.
Using LoadConfigFile Explicitly¶
For more control (e.g., loading into a sub-struct, multiple config files), use LoadConfigFile in a PreValidate hook:
type AppConfig struct {
Host string
Port int
}
type Params struct {
ConfigFile string `descr:"Path to config file" optional:"true"`
AppConfig
}
boa.CmdT[Params]{
Use: "app",
PreValidateFunc: func(p *Params, cmd *cobra.Command, args []string) error {
return boa.LoadConfigFile(p.ConfigFile, &p.AppConfig, nil)
},
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Host: %s, Port: %d\n", p.Host, p.Port)
},
}.Run()
Checking Value Sources¶
Use HookContext in your run function to check how values were set:
boa.CmdT[Params]{
Use: "app",
RunFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command, args []string) {
if ctx.HasValue(&p.Port) {
fmt.Printf("Port explicitly set to %d\n", p.Port)
} else {
fmt.Println("Using default port")
}
},
}
Accessing Cobra Directly¶
Access the underlying Cobra command for features BOA doesn't wrap:
boa.CmdT[Params]{
Use: "app",
InitFunc: func(p *Params, cmd *cobra.Command) error {
cmd.Deprecated = "use 'new-app' instead"
cmd.Hidden = true
cmd.SilenceUsage = true
return nil
},
}
Or after flags are created:
boa.CmdT[Params]{
Use: "app",
PostCreateFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
flag := cmd.Flags().Lookup("verbose")
flag.NoOptDefVal = "true" // --verbose without value means true
return nil
},
}
Command Groups¶
Organize subcommands into groups in help output:
boa.CmdT[boa.NoParams]{
Use: "app",
Groups: []*cobra.Group{
{ID: "core", Title: "Core Commands:"},
{ID: "util", Title: "Utility Commands:"},
},
SubCmds: boa.SubCmds(
boa.CmdT[Params]{Use: "init", GroupID: "core"},
boa.CmdT[Params]{Use: "run", GroupID: "core"},
boa.CmdT[Params]{Use: "version", GroupID: "util"},
),
}
Testing Commands¶
Inject Arguments¶
cmd := boa.CmdT[Params]{
Use: "app",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
// ...
},
}
// Test with specific args
cmd.RunArgs([]string{"--name", "test", "--port", "8080"})
Testing with Error Returns¶
Use RunFuncE and RunArgsE for testable commands that return errors:
func TestMyCommand(t *testing.T) {
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
},
}.RunArgsE([]string{"--port", "80"})
if err == nil {
t.Fatal("expected error for port < 1024")
}
}
Use ToCobraE() when you need the underlying cobra command with RunE set:
func TestMyCommand(t *testing.T) {
cmd, err := boa.CmdT[Params]{
Use: "app",
RunFuncE: func(p *Params, cmd *cobra.Command, args []string) error {
return nil
},
}.ToCobraE()
if err != nil {
t.Fatalf("setup failed: %v", err)
}
cmd.SetArgs([]string{"--name", "test"})
err = cmd.Execute()
// Assert on err
}
Validate Without Running¶
cmd := boa.CmdT[Params]{
Use: "app",
RawArgs: []string{"--name", "test"},
}
err := cmd.Validate()
if err != nil {
// Validation failed
}
Interface-Based Hooks¶
Implement interfaces on your config struct instead of using function fields:
type Config struct {
Host string
Port int
}
// Called during initialization
func (c *Config) Init() error {
return nil
}
// Called during initialization with HookContext
func (c *Config) InitCtx(ctx *boa.HookContext) error {
ctx.GetParam(&c.Port).SetDefault(boa.Default(8080))
return nil
}
// Called before validation
func (c *Config) PreValidate() error {
return nil
}
// Called before execution
func (c *Config) PreExecute() error {
return nil
}
Available interfaces:
CfgStructInit-Init() errorCfgStructInitCtx-InitCtx(ctx *HookContext) errorCfgStructPostCreate-PostCreate() errorCfgStructPostCreateCtx-PostCreateCtx(ctx *HookContext) errorCfgStructPreValidate-PreValidate() errorCfgStructPreValidateCtx-PreValidateCtx(ctx *HookContext) errorCfgStructPreExecute-PreExecute() errorCfgStructPreExecuteCtx-PreExecuteCtx(ctx *HookContext) error
JSON Fallback for Complex Types¶
Any field type that doesn't have native pflag support (e.g., nested slices, complex maps) automatically falls back to JSON parsing. BOA registers the flag as a StringP and uses json.Unmarshal to parse the value.
This means you can use arbitrarily complex types in your params struct:
type Params struct {
Matrix [][]int `descr:"nested matrix" optional:"true"`
Meta map[string][]string `descr:"metadata" optional:"true"`
Config map[string]any `descr:"arbitrary config" optional:"true"`
}
// CLI usage:
// --matrix '[[1,2],[3,4]]'
// --meta '{"tags":["a","b"],"owners":["alice"]}'
// --config '{"debug":true,"retries":3}'
The same JSON syntax works for environment variables. Config files work natively since they are already unmarshaled from JSON/YAML/etc.
Simple Maps¶
Simple map types like map[string]string and map[string]int use the more ergonomic key=val,key=val syntax on the CLI, and only fall back to JSON for complex value types:
type Params struct {
Labels map[string]string `descr:"simple key=val syntax"`
Deep map[string][]string `descr:"JSON syntax required"`
}
// --labels env=prod,team=backend
// --deep '{"groups":["admin","users"]}'
Config-File-Only Fields with boa:"ignore" / boa:"configonly"¶
Fields tagged with boa:"ignore" (or its alias boa:"configonly") are excluded from CLI flag and environment variable registration. They won't appear in --help and can't be set via the command line.
However, config file loading uses json.Unmarshal (or your configured unmarshal function) which writes directly to struct fields, bypassing the CLI layer entirely. This means ignored fields are still populated from config files.
This pattern is useful for fields that only make sense in a config file:
type Params struct {
ConfigFile string `configfile:"true" optional:"true" default:"config.json"`
Host string `descr:"server host"`
Port int `descr:"server port"`
InternalID string `boa:"configonly"` // only from config file
Metadata map[string]string `boa:"configonly"` // complex config, not a CLI concern
}
boa:"configonly" is functionally identical to boa:"ignore" but communicates intent more clearly.
With config.json:
{
"Host": "example.com",
"Port": 8080,
"InternalID": "abc-123",
"Metadata": {"version": "2", "region": "us-east-1"}
}
Host and Port can be overridden via CLI flags; InternalID and Metadata are only loaded from the config file.
Named Struct Auto-Prefixing¶
Named (non-anonymous) struct fields automatically prefix their children's flag names and env var names with the field name in kebab-case. This is the primary mechanism for avoiding flag name collisions when reusing struct types.
How It Works¶
type DBConfig struct {
Host string `descr:"database host" default:"localhost"`
Port int `descr:"database port" default:"5432"`
}
type Params struct {
Primary DBConfig // --primary-host, --primary-port, env: PRIMARY_HOST, PRIMARY_PORT
Replica DBConfig // --replica-host, --replica-port, env: REPLICA_HOST, REPLICA_PORT
}
Prefixing Rules¶
| Scenario | Flag Name | Env Var |
|---|---|---|
Embedded DBConfig with Host |
--host |
HOST |
Named DB DBConfig with Host |
--db-host |
DB_HOST |
Deep Infra.Primary.Host |
--infra-primary-host |
INFRA_PRIMARY_HOST |
Named field + explicit name:"host" |
--db-host (prefixed) |
N/A |
Named field + explicit env:"SERVER_HOST" |
N/A | DB_SERVER_HOST (prefixed) |
Embedded + explicit env:"MY_HOST" |
N/A | MY_HOST (not prefixed) |
Deep Nesting¶
Prefixes chain at every named (non-anonymous) level:
type ConnectionConfig struct {
Host string `default:"localhost"`
Port int `default:"5432"`
}
type ClusterConfig struct {
Primary ConnectionConfig
Replica ConnectionConfig
}
type Params struct {
Infra ClusterConfig
}
// Flags: --infra-primary-host, --infra-primary-port, --infra-replica-host, --infra-replica-port
// Env vars: INFRA_PRIMARY_HOST, INFRA_PRIMARY_PORT, etc.
Explicit Tags Are Also Prefixed¶
Inside a named struct field, both auto-generated and explicit tag values get the parent prefix. This is intentional -- it avoids collisions when the same struct type appears multiple times:
type ServerConfig struct {
Host string `name:"host" env:"SERVER_HOST" default:"localhost"`
}
type Params struct {
API ServerConfig // flag: --api-host, env: API_SERVER_HOST
Web ServerConfig // flag: --web-host, env: WEB_SERVER_HOST
}
Custom Type Registration¶
Register user-defined types as CLI parameters with RegisterType. The type is stored as a string flag in cobra and converted via your provided Parse/Format functions:
type SemVer struct {
Major, Minor, Patch int
}
func init() {
boa.RegisterType[SemVer](boa.TypeDef[SemVer]{
Parse: func(s string) (SemVer, error) {
var v SemVer
_, err := fmt.Sscanf(s, "%d.%d.%d", &v.Major, &v.Minor, &v.Patch)
return v, err
},
Format: func(v SemVer) string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
},
})
}
type Params struct {
Version SemVer `descr:"app version" default:"1.0.0"`
}
TypeDef[T] fields:
| Field | Type | Description |
|---|---|---|
Parse |
func(string) (T, error) |
Converts a CLI string into the typed value (required) |
Format |
func(T) string |
Converts the typed value back to a string for default display. If nil, fmt.Sprintf("%v", val) is used |
ConfigFormatExtensions¶
boa.ConfigFormatExtensions() returns the file extensions that have registered config format handlers. Always includes .json (registered by default). This is used by the boaviper subpackage for auto-discovery:
Type Handler Registry (Architecture)¶
Internally, BOA uses a type handler registry (type_handler.go) instead of scattered type switches. Each handler provides:
bindFlag-- how to create a cobra/pflag flag for this typeparse-- how to convert a string value into the target typeconvert-- optional post-parse conversion (e.g., for types stored as strings in cobra)
Handlers are registered by exact type (for special types like time.Time, net.IP) or by reflect.Kind (for basic types like string, int). Map types use composed handlers that delegate value parsing to the appropriate scalar handler for their value type.
Types without a registered handler fall back to StringP + json.Unmarshal, which is how nested slices and complex maps are supported automatically.