clic

package module
v0.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2025 License: MIT Imports: 7 Imported by: 0

README

Go Reference License Status Security Status Tests codecov

CLIC

clic is short for CLI Config. It implements a framework to load/parse cli configuration from a file, environment or flags.

See Go Reference for the usage.

License

FOSSA Status

Documentation

Overview

package clic is short for CLI Config. It implements a framework to load/parse cli configuration from a file, environment or flags.

Usage:

  • Register functions (Register and RegisterCallback) should be called in `func init()` in packages, or before calling Parse.
  • Parse function should be called at the beginning of "main()", before calling functions in other packages.
  • Parse function must not be called in `func init()`, because other sub-packages may not finish initialization at that time.

See examples for the usage.

Example
package main

import (
	"context"
	"flag"
	"fmt"
	"log/slog"
	"os"
	"time"

	"github.com/googollee/clic"
)

func main() {
	// structs
	type Database struct {
		Driver string `clic:"driver,sqlite3,the driver of the database [sqlite3,mysql,postgres]"`
		URL    string `clic:"url,./database.sqlite,the url of the database"`
	}
	type Log struct {
		Level slog.Level `clic:"level,INFO,the level of the log [DEBUG,INFO,WARN,ERROR]"`
	}
	initLog := func(ctx context.Context, log *Log) error {
		fmt.Println("set log level:", log.Level)
		return nil
	}

	// args
	oldArgs := os.Args
	os.Args = []string{oldArgs[0], "-database.driver", "driver", "-database.url", "url", "sub_command"}
	defer func() {
		os.Args = oldArgs
	}()

	// main code
	ctx := context.Background()

	var db Database
	clic.Register("database", &db)
	clic.RegisterCallback("log", initLog)

	// config should be finished in a minute.
	configCtx, cancel := context.WithTimeout(ctx, time.Minute)
	defer cancel()
	clic.Parse(configCtx)

	fmt.Println("database:", db)
	fmt.Println("remain args:", flag.Args())

}
Output:

set log level: INFO
database: {driver url}
remain args: [sub_command]
Example (EmbedStruct)
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"

	"github.com/googollee/clic"
)

func main() {
	// prepare env
	for _, key := range []string{"DEMO_VALUE"} {
		if err := os.Setenv(key, "value_from_env"); err != nil {
			log.Fatal("set env error:", err)
		}
	}

	// code starts
	type Inner struct {
		Value string `clic:"value,default,the value in the inner struct"`
	}

	type Config struct {
		Value string `clic:"value,default,the value in the outer struct"`
		Inner Inner  `clic:"inner"`
	}

	fset := flag.NewFlagSet("", flag.PanicOnError)
	set := clic.NewSet(fset)

	var cfg Config
	set.RegisterValue("demo", &cfg)

	ctx := context.Background()
	if err := set.Parse(ctx, []string{"-demo.inner.value", "value_from_flag"}); err != nil {
		log.Fatal("parse error:", err)
	}

	fmt.Println("Value:", cfg.Value)
	fmt.Println("Inner.Value:", cfg.Inner.Value)

}
Output:

Value: value_from_env
Inner.Value: value_from_flag

Index

Examples

Constants

This section is empty.

Variables

Functions

func Parse

func Parse(ctx context.Context)

Parse parses configuration from DefaultSources and os.Args.

If any error happens during calling, "Parse()" prints that error on Stderr and calls os.Exit to exit with "125" code.

func Register

func Register(prefix string, value any)

Register registers a "Config" value with the "name" as the scope name. The value is filled after calling Parse function.

Example:

package main

func main() {
	ctx := context.Background()

	var dbCfg database.Config
	clic.Register("database", &db)

	clic.Parse(ctx)

	db := database.New(dbCfg)
}

func RegisterCallback

func RegisterCallback(prefix string, f any)

RegisterCallback registers a callback function with the "name" as the scope name. The callback is called after calling Parse function.

Example:

package main

type Log struct {
	Level slog.Level `clic:"level,INFO,the level of log"`
}

func initLogLevel(ctx context.Context, cfg *Log) {
	slog.SetLevel(cfg.Level)
}

func main() {
	ctx := context.Background()

	clic.RegisterCallback("log", initLogLevel)

	clic.Parse(ctx)
}

func RegisterType added in v0.1.5

func RegisterType[Config any](name string) func(ctx context.Context) *Config

RegisterType registers a "Config" type with the "name" as the scope name and returns a getter function which returns a parsed Config value.

Example:

package main

func main() {
	ctx := context.Background()

	loadCfg := clic.RegisterType[database.Config]()

	clic.Parse(ctx)

	db := database.New(loadCfg())
}

Types

type Set

type Set struct {
	// contains filtered or unexported fields
}
Example
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"log/slog"

	"github.com/googollee/clic"
	"github.com/googollee/clic/source"
)

func main() {
	// structs
	type Database struct {
		Driver string `clic:"driver,sqlite3,the driver of the database [sqlite3,mysql,postgres]"`
		URL    string `clic:"url,./database.sqlite,the url of the database"`
	}
	type Log struct {
		Level slog.Level `clic:"level,INFO,the level of the log [DEBUG,INFO,WARN,ERROR]"`
	}
	initLog := func(ctx context.Context, log *Log) error {
		fmt.Println("set log level:", log.Level)
		return nil
	}

	// set with sources
	fset := flag.NewFlagSet("", flag.ExitOnError)
	set := clic.NewSet(fset,
		// The order means srouce priority, flag > config file > env
		source.Flag(source.FlagSplitter(".")),
		source.File(source.FilePathFlag("config"), source.FileFormat(source.JSON{})),
		source.Env(source.EnvSplitter("_")),
	)

	// args
	args := []string{"-log.level", "WARN", "-database.driver", "driver", "-database.url", "url", "other_cmd"}

	// main code
	ctx := context.Background()

	var db Database
	set.RegisterValue("database", &db)
	set.RegisterCallback("log", initLog)

	if err := set.Parse(ctx, args); err != nil {
		log.Fatal("parse error:", err)
	}

	fmt.Println("database:", db)
	fmt.Println("remain args:", fset.Args())

}
Output:

set log level: WARN
database: {driver url}
remain args: [other_cmd]
Example (ShowHelp)
package main

import (
	"bytes"
	"context"
	"errors"
	"flag"
	"fmt"
	"log"
	"log/slog"
	"strings"

	"github.com/googollee/clic"
	"github.com/googollee/clic/source"
)

func main() {
	// structs
	type Database struct {
		Driver string `clic:"driver,sqlite3,the driver of the database [sqlite3,mysql,postgres]"`
		URL    string `clic:"url,./database.sqlite,the url of the database"`
	}
	type Log struct {
		Level slog.Level `clic:"level,INFO,the level of the log [DEBUG,INFO,WARN,ERROR]"`
	}
	initLog := func(ctx context.Context, log *Log) error {
		fmt.Println("set log level:", log.Level)
		return nil
	}

	// set with sources
	fset := flag.NewFlagSet("", flag.ContinueOnError)
	var helpOutput bytes.Buffer
	fset.SetOutput(&helpOutput)
	set := clic.NewSet(fset,
		// The order means srouce priority, flag > config file > env
		source.Flag(source.FlagSplitter(".")),
		source.File(source.FilePathFlag("config"), source.FileFormat(source.JSON{})),
		source.Env(source.EnvSplitter("_")),
	)

	// args
	args := []string{"-h"}

	// main code
	ctx := context.Background()

	var db Database
	set.RegisterValue("database", &db)
	set.RegisterCallback("log", initLog)

	if err := set.Parse(ctx, args); !errors.Is(err, flag.ErrHelp) {
		log.Fatal("parse error:", err)
	}

	fmt.Println(strings.ReplaceAll(helpOutput.String(), "\t", "    "))
}
Output:

Usage:
  -config string
        the path of the config file
  -database.driver value
        the driver of the database [sqlite3,mysql,postgres] (default sqlite3)
  -database.url value
        the url of the database (default ./database.sqlite)
  -log.level value
        the level of the log [DEBUG,INFO,WARN,ERROR] (default INFO)
Example (SourcePriorities)
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"

	"github.com/googollee/clic"
)

func main() {
	// prepare env
	for _, key := range []string{"DEMO_VALUE_FLAG", "DEMO_VALUE_ENV", "DEMO_VALUE_FILE"} {
		if err := os.Setenv(key, "value_from_env"); err != nil {
			log.Fatal("set env error:", err)
		}
	}

	// prepare config file
	cfgFile, err := os.CreateTemp("", "config_*.json")
	if err != nil {
		log.Fatal("create temp file error:", err)
	}
	defer os.Remove(cfgFile.Name())

	if _, err := cfgFile.WriteString(`{
		"demo": {
			"value_flag": "value_from_file",
			"value_file": "value_from_file"
		}
	}`); err != nil {
		log.Fatal("write temp file error:", err)
	}

	if err := cfgFile.Close(); err != nil {
		log.Fatal("close temp file error:", err)
	}

	// prepare args
	args := []string{"-config", cfgFile.Name(), "-demo.value_flag", "value_from_flag"}

	// code starts
	type Config struct {
		ValueFlag    string `clic:"value_flag,default,a test value in flag"`
		ValueEnv     string `clic:"value_env,default,a test value in env"`
		ValueFile    string `clic:"value_file,default,a test value in config file"`
		ValueDefault string `clic:"value_default,default,a test value by default"`
	}
	var cfg Config

	fset := flag.NewFlagSet("", flag.PanicOnError)
	set := clic.NewSet(fset, clic.DefaultSources...)

	set.RegisterValue("demo", &cfg)

	ctx := context.Background()
	if err := set.Parse(ctx, args); err != nil {
		log.Fatal("parse error:", err)
	}

	fmt.Println("ValueFlag:", cfg.ValueFlag)
	fmt.Println("ValueEnv:", cfg.ValueEnv)
	fmt.Println("ValueFile:", cfg.ValueFile)
	fmt.Println("ValueDefault:", cfg.ValueDefault)

}
Output:

ValueFlag: value_from_flag
ValueEnv: value_from_env
ValueFile: value_from_file
ValueDefault: default

func NewSet

func NewSet(fset source.FlagSet, source ...source.Source) *Set

func (*Set) Parse

func (s *Set) Parse(ctx context.Context, args []string) error

func (*Set) RegisterCallback

func (s *Set) RegisterCallback(prefix string, callback any)

func (*Set) RegisterValue

func (s *Set) RegisterValue(prefix string, value any)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL