Paerser is a Go library for loading configuration into structs from multiple sources:
- CLI flag
- Configuration files
- Environment variables
It also provides a lightweight CLI command system with sub-commands, automatic help generation, and resource loaders that chain these configuration sources together.
Note
This fork is created to support the needs of the Tinyauth. Over time, we may remove some features that are not relevant to our use case, and we may also add new features that we need. If you want to contribute or have any questions, please feel free to open an issue or a pull request.
Before you ask, yes the readme is generated by an LLM because LLM readme is better than no readme ;)
go get github.com/tinyauthapp/paerser
Define a configuration struct and populate it from any source:
package main
import (
"fmt"
"log"
"os"
"github.com/tinyauthapp/paerser/env"
"github.com/tinyauthapp/paerser/file"
"github.com/tinyauthapp/paerser/flag"
)
type Config struct {
Host string
Port int
DB DatabaseConfig
}
type DatabaseConfig struct {
DSN string
MaxConn int
}
func main() {
cfg := Config{}
// From flags:
err := flag.Decode([]string{"--host=localhost", "--port=8080", "--db.dsn=postgres://localhost/mydb"}, &cfg)
// From environment variables:
err = env.Decode(os.Environ(), "MYAPP_", &cfg)
// From a file (YAML, TOML, or JSON):
err = file.Decode("config.yml", &cfg)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", cfg)
}Decodes command-line flag arguments into a struct using dot-separated names that mirror the struct hierarchy.
package main
import (
"fmt"
"log"
"github.com/tinyauthapp/paerser/flag"
)
type Config struct {
Server ServerConfig
Debug bool
}
type ServerConfig struct {
Host string
Port int
Tags []string
}
func main() {
args := []string{
"--server.host=0.0.0.0",
"--server.port=9090",
"--server.tags=web,api",
"--debug",
}
cfg := Config{}
if err := flag.Decode(args, &cfg); err != nil {
log.Fatal(err)
}
fmt.Println(cfg.Server.Host) // "0.0.0.0"
fmt.Println(cfg.Server.Port) // 9090
fmt.Println(cfg.Server.Tags) // [web api]
fmt.Println(cfg.Debug) // true
}*Flag syntax rules:
| Syntax | Behavior |
|---|---|
--flag value or --flag=value |
Standard flag with a value |
-flag value or -flag=value |
Short-form (single dash) |
--boolflag |
Boolean flags are set to true implicitly |
--slice=a,b,c |
Comma-separated values populate slices |
-- |
Stops flag parsing |
Encode a struct back into a flat list of flag definitions (useful for help text generation):
flats, err := flag.Encode(&cfg)
// Each flat has a Name (e.g. "server.host") and Default value.If you only need the raw key-value map without populating a struct:
parsed, err := flag.Parse(os.Args[1:], &cfg)
// parsed is a map[string]string, e.g. {"traefik.server.host": "0.0.0.0"}Decodes environment variables into a struct. Variable names are derived from the struct field path, uppercased, with underscores as separators.
package main
import (
"fmt"
"log"
"os"
"github.com/tinyauthapp/paerser/env"
)
type Config struct {
Host string
Port int
DB DBConfig
}
type DBConfig struct {
DSN string
MaxConn int
}
func main() {
os.Setenv("APP_HOST", "localhost")
os.Setenv("APP_PORT", "3000")
os.Setenv("APP_DB_DSN", "postgres://localhost/mydb")
os.Setenv("APP_DB_MAXCONN", "10")
cfg := Config{}
if err := env.Decode(os.Environ(), "APP_", &cfg); err != nil {
log.Fatal(err)
}
fmt.Println(cfg.Host) // "localhost"
fmt.Println(cfg.Port) // 3000
fmt.Println(cfg.DB.DSN) // "postgres://localhost/mydb"
fmt.Println(cfg.DB.MaxConn) // 10
}The prefix (e.g. "APP_") must match the pattern ^[a-zA-Z0-9]+_$ - alphanumeric characters followed by a single trailing underscore.
Encode a struct into flat environment variable representations:
flats, err := env.Encode("APP_", &cfg)
// Each flat has a Name (e.g. "APP_DB_DSN") and Default value.Find only the environment variables that are relevant to a given struct:
vars := env.FindPrefixedEnvVars(os.Environ(), "APP_", &cfg)
// Returns only env vars whose keys match known struct field paths.This is more precise than a simple prefix match — it checks that the variable name corresponds to an actual field in the struct.
Decodes YAML, TOML, or JSON configuration files into a struct. The format is detected from the file extension.
package main
import (
"fmt"
"log"
"github.com/tinyauthapp/paerser/file"
)
type Config struct {
Server ServerConfig
Debug bool
}
type ServerConfig struct {
Host string
Port int
}
func main() {
cfg := Config{}
if err := file.Decode("config.yml", &cfg); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", cfg)
}Where config.yml contains:
server:
host: localhost
port: 8080
debug: trueSupported extensions: .toml, .yaml, .yml, .json
If you already have the content in memory:
content := `
server:
host: localhost
port: 8080
`
cfg := Config{}
err := file.DecodeContent(content, ".yml", &cfg)A lightweight CLI framework that ties together flag, env, and file loaders with support for sub-commands and automatic help generation.
package main
import (
"fmt"
"log"
"github.com/tinyauthapp/paerser/cli"
)
type Config struct {
Host string `description:"Server host"`
Port int `description:"Server port"`
Debug bool `description:"Enable debug mode"`
}
func main() {
cfg := &Config{}
cmd := &cli.Command{
Name: "myapp",
Description: "My awesome application",
Configuration: cfg,
Resources: []cli.ResourceLoader{
&cli.FlagLoader{},
},
Run: func(args []string) error {
fmt.Printf("Starting server on %s:%d (debug=%v)\n", cfg.Host, cfg.Port, cfg.Debug)
return nil
},
}
if err := cli.Execute(cmd); err != nil {
log.Fatal(err)
}
}$ myapp --host=0.0.0.0 --port=8080 --debug
Starting server on 0.0.0.0:8080 (debug=true)
Resource loaders populate cmd.Configuration and are executed in order. If a loader returns done == true, the chain stops.
| Loader | Source | Description |
|---|---|---|
cli.FlagLoader{} |
CLI flags | Decodes --flag=value arguments |
cli.EnvLoader{Prefix: "MYAPP_"} |
Environment | Decodes MYAPP_* environment variables |
cli.FileLoader{...} |
Config file | Loads from a file (path given via a flag or searched from base paths) |
Chaining loaders (file first, then flags to override):
cmd := &cli.Command{
Name: "myapp",
Description: "My application",
Configuration: cfg,
Resources: []cli.ResourceLoader{
&cli.FileLoader{
ConfigFileFlag: "configFile",
BasePaths: []string{"/etc/myapp/myapp", "$HOME/.config/myapp"},
Extensions: []string{"toml", "yaml", "yml"},
},
&cli.EnvLoader{Prefix: "MYAPP_"},
&cli.FlagLoader{},
},
Run: func(args []string) error {
// cfg is now populated from file -> env -> flags (in order)
return nil
},
}$ myapp --configFile=/path/to/config.toml --port=9090
rootCmd := &cli.Command{
Name: "myapp",
Description: "My application",
}
serveCmd := &cli.Command{
Name: "serve",
Description: "Start the server",
Configuration: &ServeConfig{},
Resources: []cli.ResourceLoader{&cli.FlagLoader{}},
Run: func(args []string) error {
fmt.Println("Server started!")
return nil
},
}
migrateCmd := &cli.Command{
Name: "migrate",
Description: "Run database migrations",
Configuration: &MigrateConfig{},
Resources: []cli.ResourceLoader{&cli.FlagLoader{}},
Run: func(args []string) error {
fmt.Println("Migrations complete!")
return nil
},
}
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(migrateCmd)
cli.Execute(rootCmd)$ myapp serve --port=8080
$ myapp migrate --db.dsn=postgres://localhost/mydb
Help is generated automatically from the struct's flags and descriptions. Pass --help or -h to any command:
$ myapp --help
myapp My application
Usage: myapp [command] [flags] [arguments]
Use "myapp [command] --help" for help on any command.
Commands:
serve Start the server
migrate Run database migrations
You can also provide a custom help function:
cmd := &cli.Command{
CustomHelpFunc: func(w io.Writer, cmd *cli.Command) error {
fmt.Fprintf(w, "Custom help for %s\n", cmd.Name)
return nil
},
}Recursively initializes all pointer and struct fields in a configuration struct, calling SetDefaults() on any field that implements the initializer interface.
package main
import (
"fmt"
"github.com/tinyauthapp/paerser/generator"
)
type Config struct {
Server *ServerConfig
}
type ServerConfig struct {
Host string
Port int
}
func (s *ServerConfig) SetDefaults() {
s.Host = "localhost"
s.Port = 8080
}
func main() {
cfg := &Config{}
generator.Generate(cfg)
fmt.Println(cfg.Server.Host) // "localhost"
fmt.Println(cfg.Server.Port) // 8080
}Generate will:
- Allocate nil pointers (so
*ServerConfigis no longer nil) - Call
SetDefaults()on any field whose pointer type implements it - Recurse into nested structs, maps, and slices
A duration type that works seamlessly with TOML, YAML, JSON, and plain integer values (interpreted as seconds).
package main
import (
"fmt"
"github.com/tinyauthapp/paerser/types"
)
func main() {
var d types.Duration
d.Set("30s") // 30 seconds
d.Set("5m30s") // 5 minutes and 30 seconds
d.Set("120") // 120 seconds (suffix-less integers are treated as seconds)
fmt.Println(d.String()) // "2m0s"
}It implements encoding.TextMarshaler, encoding.TextUnmarshaler, json.Marshaler, and json.Unmarshaler, so it works out of the box in configuration files:
timeout: 30s
interval: 120The parser package is the foundation that all other packages build on. It provides a tree-based intermediate representation (Node) for configuration data, along with encoding/decoding utilities.
Most users won't need to use this package directly, but it's available for advanced use cases:
// Decode a flat label map into a struct
labels := map[string]string{
"traefik.http.routers.myrouter.rule": "Host(`example.com`)",
"traefik.http.routers.myrouter.tls": "true",
}
err := parser.Decode(labels, &cfg, "traefik")
// Encode a struct into a flat label map
labels, err := parser.Encode(&cfg, "traefik")Key types and functions:
Node- A recursive tree node withName,Value,Kind,Children, etc.Decode(labels, element, rootName)- Flat map -> structEncode(element, rootName)- Struct -> flat mapFlat- A flattened key/value/default representation used byflag.Encodeandenv.Encode
The library uses struct tags to control field naming and behavior in different contexts:
| Tag | Used By | Purpose |
|---|---|---|
description |
cli (help generation) |
Description shown in --help output |
label |
parser, generator |
Controls label-based encoding/decoding. Use "-" to skip a field |
file |
file |
Controls file-based decoding |