Advanced Examples¶
Examples covering custom types, complex data structures, nested structs, subcommands, validation, and shell completion.
Custom Types with RegisterType¶
Register any Go type as a CLI parameter by providing parse and format functions.
SemVer Example¶
package main
import (
"fmt"
"strconv"
"strings"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type SemVer struct {
Major, Minor, Patch int
}
func (v SemVer) String() string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}
func parseSemVer(s string) (SemVer, error) {
parts := strings.SplitN(s, ".", 3)
if len(parts) != 3 {
return SemVer{}, fmt.Errorf("expected MAJOR.MINOR.PATCH, got %q", s)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return SemVer{}, fmt.Errorf("invalid major version: %w", err)
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return SemVer{}, fmt.Errorf("invalid minor version: %w", err)
}
patch, err := strconv.Atoi(parts[2])
if err != nil {
return SemVer{}, fmt.Errorf("invalid patch version: %w", err)
}
return SemVer{Major: major, Minor: minor, Patch: patch}, nil
}
type Params struct {
Version SemVer `descr:"Application version" default:"0.1.0"`
}
func main() {
boa.RegisterType[SemVer](boa.TypeDef[SemVer]{
Parse: parseSemVer,
Format: func(v SemVer) string { return v.String() },
})
boa.CmdT[Params]{
Use: "release",
Short: "Create a release",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Releasing version %s\n", p.Version)
fmt.Printf("Major: %d, Minor: %d, Patch: %d\n",
p.Version.Major, p.Version.Minor, p.Version.Patch)
},
}.Run()
}
$ go run . --version 2.1.0
Releasing version 2.1.0
Major: 2, Minor: 1, Patch: 0
$ go run .
Releasing version 0.1.0
Major: 0, Minor: 1, Patch: 0
$ go run . --version not-valid
# Error: invalid value for param version: expected MAJOR.MINOR.PATCH, got "not-valid"
LogLevel Example¶
package main
import (
"fmt"
"strings"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type LogLevel int
const (
Debug LogLevel = iota
Info
Warn
Error
)
func (l LogLevel) String() string {
switch l {
case Debug:
return "debug"
case Info:
return "info"
case Warn:
return "warn"
case Error:
return "error"
default:
return "unknown"
}
}
func parseLogLevel(s string) (LogLevel, error) {
switch strings.ToLower(s) {
case "debug":
return Debug, nil
case "info":
return Info, nil
case "warn", "warning":
return Warn, nil
case "error":
return Error, nil
default:
return Info, fmt.Errorf("unknown log level %q (use debug, info, warn, error)", s)
}
}
type Params struct {
Level LogLevel `descr:"Log level" default:"info"`
}
func main() {
boa.RegisterType[LogLevel](boa.TypeDef[LogLevel]{
Parse: parseLogLevel,
Format: func(l LogLevel) string { return l.String() },
})
boa.CmdT[Params]{
Use: "logger",
Short: "Demo custom log level type",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Log level: %s (%d)\n", p.Level, p.Level)
},
}.Run()
}
Optional Custom Types with Pointers¶
Pointer-to-custom-type fields are optional by default, just like pointer-to-primitive fields. nil means not provided.
Map Fields¶
Simple Key-Value Maps¶
Simple maps use the ergonomic key=val,key=val syntax on the CLI. Map fields default to optional.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Labels map[string]string `descr:"Key-value labels"`
Ports map[string]int `descr:"Named ports"`
}
func main() {
boa.CmdT[Params]{
Use: "deploy",
Short: "Deploy with labels and ports",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Println("Labels:")
for k, v := range p.Labels {
fmt.Printf(" %s = %s\n", k, v)
}
fmt.Println("Ports:")
for k, v := range p.Ports {
fmt.Printf(" %s = %d\n", k, v)
}
},
}.Run()
}
$ go run . --labels env=prod,team=backend --ports http=80,https=443
Labels:
env = prod
team = backend
Ports:
http = 80
https = 443
Maps from Environment Variables¶
Maps from Config Files¶
Maps are populated naturally from JSON config files:
type Params struct {
ConfigFile string `configfile:"true" optional:"true" default:"config.json"`
Labels map[string]string `descr:"Labels" optional:"true"`
}
Complex Types with JSON on CLI¶
Types without native pflag support (nested slices, maps with complex values) automatically fall back to JSON parsing.
Nested Slices (Matrix)¶
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Matrix [][]int `descr:"Data matrix"`
}
func main() {
boa.CmdT[Params]{
Use: "matrix",
Short: "Process a data matrix",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
for i, row := range p.Matrix {
fmt.Printf("Row %d: %v\n", i, row)
}
},
}.Run()
}
Complex Map Types¶
Arbitrary Nested Structures¶
JSON via Environment Variables¶
The same JSON syntax works for env vars:
Nested Struct Composition with Auto-Prefixing¶
Named (non-anonymous) struct fields auto-prefix their children's flag names. This prevents collisions when reusing the same struct type.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type ConnectionConfig struct {
Host string `descr:"Hostname" default:"localhost"`
Port int `descr:"Port number" default:"5432"`
Username string `descr:"Username" default:"admin"`
}
type Params struct {
Primary ConnectionConfig // --primary-host, --primary-port, --primary-username
Replica ConnectionConfig // --replica-host, --replica-port, --replica-username
}
func main() {
boa.CmdT[Params]{
Use: "db",
Short: "Database connection manager",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Primary: %s@%s:%d\n", p.Primary.Username, p.Primary.Host, p.Primary.Port)
fmt.Printf("Replica: %s@%s:%d\n", p.Replica.Username, p.Replica.Host, p.Replica.Port)
},
}.Run()
}
$ go run . --primary-host db1.internal --replica-host db2.internal
Primary: admin@db1.internal:5432
Replica: admin@db2.internal:5432
$ go run . --primary-host db1 --primary-port 5433 --replica-host db2 --replica-username readonly
Primary: admin@db1:5433
Replica: readonly@db2:5432
Deep Nesting (3+ Levels)¶
Prefixes chain at every named level:
type ConnectionConfig struct {
Host string `descr:"Hostname" default:"localhost"`
Port int `descr:"Port number" 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
Explicit Tags Are Also Prefixed¶
Inside named struct fields, explicit name and env tags are prefixed too. This avoids collisions when the same struct appears multiple times:
type ServerConfig struct {
Host string `name:"host" env:"SERVER_HOST" default:"localhost"`
Port int `name:"port" env:"SERVER_PORT" default:"8080"`
}
type Params struct {
API ServerConfig // --api-host, --api-port, env: API_SERVER_HOST, API_SERVER_PORT
Web ServerConfig // --web-host, --web-port, env: WEB_SERVER_HOST, WEB_SERVER_PORT
}
Embedded Structs for Shared Options¶
Embedded (anonymous) struct fields are NOT prefixed. Use this to share common options across commands.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type CommonOpts struct {
Verbose bool `descr:"Verbose output" short:"v" optional:"true"`
Format string `descr:"Output format" alts:"json,text,table" default:"text"`
}
type ListParams struct {
CommonOpts // embedded -- flags: --verbose, --format (no prefix)
Limit int `descr:"Max items" default:"50"`
}
type GetParams struct {
CommonOpts // embedded -- same flags, no prefix
ID string `descr:"Item ID"`
}
func main() {
boa.CmdT[boa.NoParams]{
Use: "items",
Short: "Manage items",
SubCmds: boa.SubCmds(
boa.CmdT[ListParams]{
Use: "list",
Short: "List items",
RunFunc: func(p *ListParams, cmd *cobra.Command, args []string) {
fmt.Printf("Listing %d items (format=%s, verbose=%v)\n",
p.Limit, p.Format, p.Verbose)
},
},
boa.CmdT[GetParams]{
Use: "get",
Short: "Get an item",
RunFunc: func(p *GetParams, cmd *cobra.Command, args []string) {
fmt.Printf("Getting item %s (format=%s, verbose=%v)\n",
p.ID, p.Format, p.Verbose)
},
},
),
}.Run()
}
$ go run . list --verbose --limit 10
Listing 10 items (format=text, verbose=true)
$ go run . get --id abc-123 --format json
Getting item abc-123 (format=json, verbose=false)
Mixing Embedded and Named¶
type Params struct {
CommonOpts // embedded -- --verbose, --format (no prefix)
DB DBConfig // named -- --db-host, --db-port (prefixed)
}
Subcommands¶
Build hierarchical CLI tools with subcommands. Use boa.NoParams for parent commands that have no parameters of their own.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type AddParams struct {
Name string `descr:"Item name"`
Count int `descr:"Quantity" default:"1"`
}
type RemoveParams struct {
ID string `descr:"Item ID"`
Force bool `descr:"Skip confirmation" optional:"true"`
}
type ListParams struct {
Limit int `descr:"Max items" default:"20"`
Format string `descr:"Output format" alts:"json,table" default:"table"`
}
func main() {
boa.CmdT[boa.NoParams]{
Use: "inventory",
Short: "Manage inventory items",
SubCmds: boa.SubCmds(
boa.CmdT[AddParams]{
Use: "add",
Short: "Add an item",
Aliases: []string{"a"},
RunFunc: func(p *AddParams, cmd *cobra.Command, args []string) {
fmt.Printf("Added %d x %s\n", p.Count, p.Name)
},
},
boa.CmdT[RemoveParams]{
Use: "remove",
Short: "Remove an item",
Aliases: []string{"rm"},
RunFunc: func(p *RemoveParams, cmd *cobra.Command, args []string) {
if p.Force {
fmt.Printf("Removed %s (forced)\n", p.ID)
} else {
fmt.Printf("Removing %s... confirm? (use --force to skip)\n", p.ID)
}
},
},
boa.CmdT[ListParams]{
Use: "list",
Short: "List items",
Aliases: []string{"ls"},
RunFunc: func(p *ListParams, cmd *cobra.Command, args []string) {
fmt.Printf("Listing up to %d items (format=%s)\n", p.Limit, p.Format)
},
},
),
}.Run()
}
$ go run . add --name "Widget" --count 5
Added 5 x Widget
$ go run . rm --id item-123 --force
Removed item-123 (forced)
$ go run . ls --format json --limit 10
Listing up to 10 items (format=json)
$ go run . --help
Manage inventory items
Usage:
inventory [command]
Available Commands:
add Add an item
list List items
remove Remove an item
Nested Subcommands¶
boa.CmdT[boa.NoParams]{
Use: "app",
SubCmds: boa.SubCmds(
boa.CmdT[boa.NoParams]{
Use: "cluster",
Short: "Cluster management",
SubCmds: boa.SubCmds(
boa.CmdT[CreateParams]{Use: "create", ...},
boa.CmdT[DeleteParams]{Use: "delete", ...},
),
},
),
}
Command Groups¶
Organize subcommands into named 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[boa.NoParams]{Use: "init", GroupID: "core", ...},
boa.CmdT[boa.NoParams]{Use: "run", GroupID: "core", ...},
boa.CmdT[boa.NoParams]{Use: "version", GroupID: "util", ...},
boa.CmdT[boa.NoParams]{Use: "config", GroupID: "util", ...},
),
}
Groups are auto-generated from subcommand GroupID values if you do not specify explicit Groups.
Validation with min/max/pattern Tags¶
BOA provides built-in validation tags for numeric ranges and string patterns.
Numeric min/max¶
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Port int `descr:"Server port" min:"1" max:"65535"`
Retries int `descr:"Max retries" min:"0" max:"10" default:"3"`
Rate float64 `descr:"Request rate" min:"0.0" max:"1.0" default:"0.5"`
}
func main() {
boa.CmdT[Params]{
Use: "server",
Short: "Start with validated params",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Port: %d, Retries: %d, Rate: %.2f\n", p.Port, p.Retries, p.Rate)
},
}.Run()
}
$ go run . --port 8080
Port: 8080, Retries: 3, Rate: 0.50
$ go run . --port 0
# Error: value 0 for param 'port' is below min 1
$ go run . --port 70000
# Error: value 70000 for param 'port' is above max 65535
$ go run . --rate 1.5
# Error: value 1.5 for param 'rate' is above max 1
String min/max (length validation)¶
For string fields, min and max validate the string length:
$ go run . --name "ab"
# Error: length 2 of param 'name' is below min 3
$ go run . --name "a-very-long-name-that-exceeds-twenty"
# Error: length 36 of param 'name' is above max 20
$ go run . --name "my-project"
# OK
String Pattern Validation¶
Use pattern for regex validation on string fields:
type Params struct {
Name string `descr:"Resource name" pattern:"^[a-z][a-z0-9-]*$"`
Tag string `descr:"Version tag" pattern:"^v[0-9]+\\.[0-9]+\\.[0-9]+$" optional:"true"`
}
$ go run . --name my-app-123
# OK
$ go run . --name MyApp
# Error: value "MyApp" for param 'name' does not match pattern ^[a-z][a-z0-9-]*$
$ go run . --name my-app --tag v1.2.3
# OK
$ go run . --name my-app --tag latest
# Error: value "latest" for param 'tag' does not match pattern ...
Validation with Pointer Fields¶
Validation tags are only checked when a value is actually provided. Pointer fields that are nil (not set) skip validation:
type Params struct {
Port *int `descr:"Port" min:"1" max:"65535"`
Tag *string `descr:"Tag" pattern:"^v[0-9]+\\.[0-9]+\\.[0-9]+$"`
}
# No flags provided -- both nil, no validation errors:
$ go run .
# Values provided -- validation runs:
$ go run . --port 0
# Error: value 0 for param 'port' is below min 1
Custom Validators¶
Use SetCustomValidatorT in InitFuncCtx for validation logic beyond what tags offer.
package main
import (
"fmt"
"net"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Host string `descr:"Server hostname"`
Port int `descr:"Server port"`
CIDR string `descr:"Allowed CIDR range" optional:"true"`
}
func main() {
boa.CmdT[Params]{
Use: "server",
Short: "Server with custom validation",
InitFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
portParam := boa.GetParamT(ctx, &p.Port)
portParam.SetCustomValidatorT(func(port int) error {
if port < 1024 && port != 80 && port != 443 {
return fmt.Errorf("non-standard privileged port %d (use 80, 443, or >= 1024)", port)
}
return nil
})
cidrParam := boa.GetParamT(ctx, &p.CIDR)
cidrParam.SetCustomValidatorT(func(cidr string) error {
if cidr == "" {
return nil
}
_, _, err := net.ParseCIDR(cidr)
if err != nil {
return fmt.Errorf("invalid CIDR: %w", err)
}
return nil
})
return nil
},
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Listening on %s:%d\n", p.Host, p.Port)
if p.CIDR != "" {
fmt.Printf("Allowed CIDR: %s\n", p.CIDR)
}
},
}.Run()
}
$ go run . --host 0.0.0.0 --port 8080
Listening on 0.0.0.0:8080
$ go run . --host 0.0.0.0 --port 22
# Error: non-standard privileged port 22 (use 80, 443, or >= 1024)
$ go run . --host 0.0.0.0 --port 443 --cidr not-a-cidr
# Error: invalid CIDR: invalid CIDR address: not-a-cidr
Conditional Required Fields¶
Make fields required only when certain conditions are met. The field must be optional:"true" in the struct tag, then use SetRequiredFn to add the condition.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Mode string `descr:"Input mode" alts:"file,http,stdin" default:"stdin"`
FilePath string `descr:"File path" optional:"true"`
URL string `descr:"HTTP URL" optional:"true"`
}
func main() {
boa.CmdT[Params]{
Use: "ingest",
Short: "Ingest data from various sources",
InitFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
ctx.GetParam(&p.FilePath).SetRequiredFn(func() bool {
return p.Mode == "file"
})
ctx.GetParam(&p.URL).SetRequiredFn(func() bool {
return p.Mode == "http"
})
return nil
},
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
switch p.Mode {
case "file":
fmt.Printf("Reading from file: %s\n", p.FilePath)
case "http":
fmt.Printf("Fetching from URL: %s\n", p.URL)
case "stdin":
fmt.Println("Reading from stdin...")
}
},
}.Run()
}
$ go run . --mode file --file-path data.csv
Reading from file: data.csv
$ go run . --mode file
# Error: required flag "file-path" not set (required because mode=file)
$ go run . --mode http --url https://api.example.com/data
Fetching from URL: https://api.example.com/data
$ go run . --mode stdin
Reading from stdin...
$ go run .
Reading from stdin...
Conditional Visibility¶
Hide parameters entirely when they are not relevant:
type Params struct {
Debug bool `descr:"Debug mode" optional:"true"`
DebugPort int `descr:"Debug port" optional:"true" default:"6060"`
TraceFile string `descr:"Trace output file" optional:"true"`
}
boa.CmdT[Params]{
Use: "server",
InitFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
// DebugPort and TraceFile only visible when Debug is true
ctx.GetParam(&p.DebugPort).SetIsEnabledFn(func() bool {
return p.Debug
})
ctx.GetParam(&p.TraceFile).SetIsEnabledFn(func() bool {
return p.Debug
})
return nil
},
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
// ...
},
}
Shell Completion with Alternatives¶
Static Alternatives¶
The alts tag provides shell completion suggestions automatically:
type Params struct {
Region string `descr:"AWS region" alts:"us-east-1,us-west-2,eu-west-1,ap-southeast-1"`
Env string `descr:"Environment" alts:"dev,staging,prod" default:"dev"`
}
With strict:"true" (the default when alts is set), invalid values are rejected. Use strict:"false" to provide suggestions without enforcing them.
Dynamic Alternatives (AlternativesFunc)¶
For completions that depend on runtime state, use SetAlternativesFunc:
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type Params struct {
Config string `descr:"Config file to use"`
}
func main() {
boa.CmdT[Params]{
Use: "app",
Short: "App with dynamic completion",
InitFuncCtx: func(ctx *boa.HookContext, p *Params, cmd *cobra.Command) error {
ctx.GetParam(&p.Config).SetAlternativesFunc(
func(cmd *cobra.Command, args []string, toComplete string) []string {
// List JSON files in the current directory
matches, _ := filepath.Glob("*.json")
return matches
},
)
ctx.GetParam(&p.Config).SetStrictAlts(false) // suggestions only
return nil
},
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {
fmt.Printf("Using config: %s\n", p.Config)
},
}.Run()
}
ValidArgsFunc for Positional Arguments¶
For positional argument completion:
boa.CmdT[Params]{
Use: "deploy",
ValidArgsFunc: func(p *Params, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return []string{"web", "api", "worker"}, cobra.ShellCompDirectiveNoFileComp
}
return nil, cobra.ShellCompDirectiveDefault
},
RunFunc: func(p *Params, cmd *cobra.Command, args []string) { ... },
}
Interface-Based Hooks¶
Instead of using function fields on the command, implement interfaces on your config struct. This keeps configuration logic co-located with the struct definition.
package main
import (
"fmt"
"github.com/GiGurra/boa/pkg/boa"
"github.com/spf13/cobra"
)
type ServerConfig struct {
Host string `descr:"Server host"`
Port int `descr:"Server port"`
LogLevel string `descr:"Log level" optional:"true"`
}
// InitCtx runs during initialization -- configure defaults and validation
func (c *ServerConfig) InitCtx(ctx *boa.HookContext) error {
ctx.GetParam(&c.Host).SetDefault(boa.Default("localhost"))
portParam := boa.GetParamT(ctx, &c.Port)
portParam.SetDefaultT(8080)
portParam.SetCustomValidatorT(func(port int) error {
if port < 1 || port > 65535 {
return fmt.Errorf("port must be between 1 and 65535")
}
return nil
})
logParam := ctx.GetParam(&c.LogLevel)
logParam.SetDefault(boa.Default("info"))
logParam.SetAlternatives([]string{"debug", "info", "warn", "error"})
logParam.SetStrictAlts(true)
return nil
}
// PreExecute runs after validation, before the command runs
func (c *ServerConfig) PreExecute() error {
fmt.Printf("[pre-execute] Will start server on %s:%d\n", c.Host, c.Port)
return nil
}
func main() {
boa.CmdT[ServerConfig]{
Use: "server",
Short: "Server with interface hooks",
RunFunc: func(p *ServerConfig, cmd *cobra.Command, args []string) {
fmt.Printf("Server running on %s:%d (log=%s)\n", p.Host, p.Port, p.LogLevel)
},
}.Run()
}
$ go run .
[pre-execute] Will start server on localhost:8080
Server running on localhost:8080 (log=info)
$ go run . --port 3000 --log-level debug
[pre-execute] Will start server on localhost:3000
Server running on localhost:3000 (log=debug)
Available interfaces:
| Interface | Method | When it runs |
|---|---|---|
CfgStructInit |
Init() error |
During initialization |
CfgStructInitCtx |
InitCtx(ctx *HookContext) error |
During initialization (with context) |
CfgStructPostCreate |
PostCreate() error |
After cobra flags created |
CfgStructPostCreateCtx |
PostCreateCtx(ctx *HookContext) error |
After cobra flags created (with context) |
CfgStructPreValidate |
PreValidate() error |
After parsing, before validation |
CfgStructPreValidateCtx |
PreValidateCtx(ctx *HookContext) error |
After parsing, before validation (with context) |
CfgStructPreExecute |
PreExecute() error |
After validation, before run |
CfgStructPreExecuteCtx |
PreExecuteCtx(ctx *HookContext) error |
After validation, before run (with context) |
Testing Commands¶
Basic Test with RunArgsE¶
func TestMyCommand(t *testing.T) {
type Params struct {
Name string `descr:"Name"`
Port int `descr:"Port" default:"8080"`
}
err := boa.CmdT[Params]{
Use: "test",
RunFuncE: func(p *Params, cmd *cobra.Command, args []string) error {
if p.Name != "alice" {
return fmt.Errorf("expected alice, got %s", p.Name)
}
if p.Port != 9090 {
return fmt.Errorf("expected 9090, got %d", p.Port)
}
return nil
},
}.RunArgsE([]string{"--name", "alice", "--port", "9090"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
Validation-Only Test¶
Test that validation catches bad input without running the command:
func TestValidation(t *testing.T) {
type Params struct {
Port int `descr:"Port" min:"1" max:"65535"`
}
err := boa.CmdT[Params]{
Use: "test",
RunFunc: func(p *Params, cmd *cobra.Command, args []string) {},
RawArgs: []string{"--port", "0"},
}.Validate()
if err == nil {
t.Fatal("expected validation error for port=0")
}
}
Testing with ToCobraE¶
Get the underlying cobra command for advanced test scenarios:
func TestWithCobra(t *testing.T) {
type Params struct {
Name string
}
cobraCmd, err := boa.CmdT[Params]{
Use: "test",
RunFuncE: func(p *Params, cmd *cobra.Command, args []string) error {
return nil
},
}.ToCobraE()
if err != nil {
t.Fatalf("setup failed: %v", err)
}
cobraCmd.SetArgs([]string{"--name", "test"})
if err := cobraCmd.Execute(); err != nil {
t.Fatalf("execution failed: %v", err)
}
}