A declarative Go library for parsing command-line arguments and environment variables into structs using struct tags. Perfect for building CLI applications with clean configuration management.
- 🏷️ Declarative: Use struct tags to define argument names, environment variables, and defaults
- 🔄 Multiple Sources: Supports command-line arguments, environment variables, and default values
- 📋 Slice Support: Parse comma-separated values into slices with configurable separators
- 🔧 Custom Parsing: Implement
encoding.TextUnmarshalerfor complex parsing logic - ⚡ Zero Dependencies: Minimal external dependencies for core functionality
- ✅ Type Safe: Supports all common Go types including pointers for optional values
- 🕐 Extended Duration: Enhanced
time.Durationparsing with support for days and weeks - 🧪 Well Tested: Comprehensive test suite with BDD-style tests
go get github.com/bborbe/argument/v2package main
import (
"context"
"fmt"
"log"
"github.com/bborbe/argument/v2"
)
func main() {
var config struct {
Username string `arg:"username" env:"USERNAME" default:"guest"`
Password string `arg:"password" env:"PASSWORD"`
Port int `arg:"port" env:"PORT" default:"8080"`
Debug bool `arg:"debug" env:"DEBUG"`
}
ctx := context.Background()
if err := argument.Parse(ctx, &config); err != nil {
log.Fatalf("Failed to parse arguments: %v", err)
}
fmt.Printf("Starting server on port %d for user %s\n", config.Port, config.Username)
}type Config struct {
Host string `arg:"host" env:"HOST" default:"localhost"`
Port int `arg:"port" env:"PORT" default:"8080"`
LogLevel string `arg:"log-level" env:"LOG_LEVEL" default:"info"`
}
var config Config
err := argument.Parse(context.Background(), &config)Run with: ./app -host=0.0.0.0 -port=9090 -log-level=debug
type DatabaseConfig struct {
Host string `arg:"db-host" env:"DB_HOST" default:"localhost"`
Port int `arg:"db-port" env:"DB_PORT" default:"5432"`
Timeout *int `arg:"timeout" env:"DB_TIMEOUT"` // Optional
MaxConns *int `arg:"max-conns" env:"DB_MAX_CONNS"` // Optional
}type ServerConfig struct {
ReadTimeout time.Duration `arg:"read-timeout" env:"READ_TIMEOUT" default:"30s"`
WriteTimeout time.Duration `arg:"write-timeout" env:"WRITE_TIMEOUT" default:"1m"`
IdleTimeout time.Duration `arg:"idle-timeout" env:"IDLE_TIMEOUT" default:"2h"`
Retention time.Duration `arg:"retention" env:"RETENTION" default:"30d"` // 30 days
BackupFreq time.Duration `arg:"backup-freq" env:"BACKUP_FREQ" default:"1w"` // 1 week
}Supported duration units: ns, us, ms, s, m, h, d (days), w (weeks)
type APIConfig struct {
APIKey string `arg:"api-key" env:"API_KEY"` // Required (no default)
Endpoint string `arg:"endpoint" env:"ENDPOINT"` // Required (no default)
Region string `arg:"region" env:"REGION" default:"us-east-1"`
}
// This will return an error if APIKey or Endpoint are not provided
err := argument.Parse(context.Background(), &config)You can use custom types (named types with underlying primitive types) for better type safety:
type Username string
type Port int
type IsEnabled bool
type Rate float64
type AppConfig struct {
Username Username `arg:"user" env:"USERNAME" default:"guest"`
Port Port `arg:"port" env:"PORT" default:"8080"`
Debug IsEnabled `arg:"debug" env:"DEBUG" default:"false"`
Rate Rate `arg:"rate" env:"RATE" default:"1.5"`
}
var config AppConfig
err := argument.Parse(context.Background(), &config)
// Access values with type safety
fmt.Printf("Username: %s\n", string(config.Username))
fmt.Printf("Port: %d\n", int(config.Port))
fmt.Printf("Debug: %t\n", bool(config.Debug))
fmt.Printf("Rate: %f\n", float64(config.Rate))Custom types work with all supported underlying types:
string→type Username stringint,int32,int64,uint,uint64→type Port intbool→type IsEnabled boolfloat64→type Rate float64
Parse comma-separated values into slices automatically:
type Config struct {
Hosts []string `arg:"hosts" env:"HOSTS" default:"localhost,127.0.0.1"`
Ports []int `arg:"ports" env:"PORTS" separator:":" default:"8080:8081:8082"`
Prices []float64 `arg:"prices" default:"9.99,19.99,29.99"`
Flags []bool `arg:"flags" default:"true,false,true"`
// Custom separator for specific use cases
Tags []string `arg:"tags" separator:"|" default:"prod|api|web"`
}
var config Config
err := argument.Parse(context.Background(), &config)Run with: ./app -hosts=server1,server2,server3 -ports=8080:9000:9001
Features:
- Default comma separator (
,) can be customized withseparator:tag - Automatic whitespace trimming around elements
- Empty string creates empty slice
- Works with custom types:
[]Username,[]Environment, etc.
Implement encoding.TextUnmarshaler for complex parsing logic:
import "encoding"
type Broker string
func (b *Broker) UnmarshalText(text []byte) error {
value := string(text)
if !strings.Contains(value, "://") {
value = "plain://" + value // Add default schema
}
*b = Broker(value)
return nil
}
type KafkaConfig struct {
Broker Broker `arg:"broker" default:"localhost:9092"`
Brokers []Broker `arg:"brokers" env:"KAFKA_BROKERS"` // Works in slices too!
}
var config KafkaConfig
err := argument.Parse(context.Background(), &config)
// broker "localhost:9092" becomes "plain://localhost:9092"
// brokers "kafka1:9092,ssl://kafka2:9093" becomes ["plain://kafka1:9092", "ssl://kafka2:9093"]Use cases:
- URL validation and normalization
- Default schema/prefix handling
- Complex validation logic
- Format conversion
- Strings:
string - Integers:
int,int32,int64,uint,uint64 - Floats:
float64 - Booleans:
bool - Durations:
time.Duration(with extended parsing) - Pointers:
*string,*int,*float64, etc. (for optional values) - Slices:
[]string,[]int,[]int64,[]uint,[]uint64,[]float64,[]bool - Custom Types: Named types with underlying primitive types
- Custom Parsing: Any type implementing
encoding.TextUnmarshaler
Values are applied with the following precedence (highest priority first):
- Command-line arguments (from
arg:tag) - Highest priority - Environment variables (from
env:tag) - Default values (from
default:tag) - Lowest priority
Command-line arguments override environment variables, which override default values.
Here's a complete example demonstrating how the priority works:
package main
import (
"context"
"fmt"
"log"
"github.com/bborbe/argument/v2"
)
func main() {
var config struct {
Port int `arg:"port" env:"PORT" default:"8080"`
}
ctx := context.Background()
if err := argument.Parse(ctx, &config); err != nil {
log.Fatalf("Failed to parse: %v", err)
}
fmt.Printf("Port: %d\n", config.Port)
}Running this program with different inputs shows the priority order:
# 1. Only default value (no env, no arg)
$ go run main.go
Port: 8080
# 2. Environment variable overrides default
$ PORT=9000 go run main.go
Port: 9000
# 3. Command-line argument overrides environment variable
$ PORT=9000 go run main.go -port=7000
Port: 7000
# 4. Command-line argument overrides default (no env set)
$ go run main.go -port=7000
Port: 7000Summary: The final value is 7000 (from -port=7000) when both env and arg are provided, demonstrating that command-line arguments have the highest priority.
Note: If no default tag is specified and neither environment variable nor argument is provided, the field will contain its Go zero value:
- Numbers:
0(int, float64, etc.) - Strings:
""(empty string) - Booleans:
false - Pointers:
nil - Slices:
nil
For complete API documentation, visit pkg.go.dev.
Parse(ctx context.Context, data interface{}) error- Parse arguments and environment variables (quiet mode)ParseAndPrint(ctx context.Context, data interface{}) error- Parse and print the final configuration valuesValidateRequired(ctx context.Context, data interface{}) error- Check that all required fields are set
Your application will automatically support standard Go flag syntax:
# Long form with equals
./app -host=localhost -port=8080
# Long form with space
./app -host localhost -port 8080
# Boolean flags
./app -debug # sets debug=true
./app -debug=false # sets debug=falseSet environment variables to configure your application:
export HOST=0.0.0.0
export PORT=9090
export DEBUG=true
./appThe library provides detailed error messages for common issues:
err := argument.Parse(ctx, &config)
if err != nil {
// Errors include context about what failed:
// - Missing required fields
// - Type conversion errors
// - Invalid duration formats
log.Fatal(err)
}This project is licensed under the BSD-style license. See the LICENSE file for details.