Go's peterbourgon ff CLI package snippet
Thu, Jul 11, 2024
2-minute read
Go’s peterbourgon ff CLI package snippet
This snippet will create a simple CLI using https://github.com/peterbourgon/ff.
It will read CLI arguments in the following order:
- flag
- environment variable
- config file
This isn’t perfect (its a POC) but is a good starting point.
package main
import (
"context"
"fmt"
"github.com/peterbourgon/ff/v4"
"github.com/peterbourgon/ff/v4/ffhelp"
"github.com/peterbourgon/ff/v4/ffyaml"
"log"
"os"
"os/signal"
"path/filepath"
"text/template"
)
const appName = "pvenotify"
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()
ret := run(ctx)
os.Exit(ret)
}
type config struct {
username string
password string
host string
noTLSVerify bool
verbose bool
config string
help bool
}
func run(ctx context.Context) int {
var cfg config
userConfigDir, err := os.UserConfigDir()
if err != nil {
log.Fatalf("could not determine user config dir: %v", err)
}
pveConfigDir := fmt.Sprintf("%s/%s", userConfigDir, "pvconfig")
err = CreateDirectoryIfNotExist(pveConfigDir)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "could not create pveconfig dir: %v\n", err)
}
fd := FileData{cfg}
tfile, err := os.ReadFile("./config.yaml")
tmpl := template.Must(template.New("config").Parse(string(tfile)))
err = generateDefaultConfigFile(pveConfigDir, tmpl, fd)
fs := ff.NewFlagSet(appName)
fs.StringVar(&cfg.username, 'u', "username", "un", "username for authentication")
fs.StringVar(&cfg.password, 'p', "password", "pw", "password for authentication")
fs.StringVar(&cfg.host, 'a', "api", "http://localhost:8006/api2/json", "PVE api host address")
fs.BoolVarDefault(&cfg.verbose, 'v', "verbose", false, "verbose logging")
fs.BoolVarDefault(&cfg.noTLSVerify, 't', "no-tls-verify", false, "do not verify TLS connections")
fs.StringVar(&cfg.config, 'c', "config", fmt.Sprintf("%s/config.yaml", pveConfigDir), "location of config file")
fs.BoolVarDefault(&cfg.help, 'h', "help", false, "show help information")
root := &ff.Command{
Name: appName,
Flags: fs,
Usage: fmt.Sprintf("%s [OPTIONS]", appName),
Subcommands: nil,
Exec: func(_ context.Context, args []string) error {
if cfg.help {
_, _ = fmt.Fprintln(os.Stderr, ffhelp.Flags(fs))
return nil
}
fmt.Printf("Config: %+v\n", cfg)
return nil
},
}
if err := root.ParseAndRun(
ctx,
os.Args[1:],
ff.WithEnvVarPrefix("PVE"),
ff.WithEnvVars(),
ff.WithConfigFileFlag("config"),
ff.WithConfigAllowMissingFile(),
ff.WithConfigFileParser(ffyaml.Parser{}.Parse),
); err != nil {
log.Fatalf("failed to parse config: %v", err)
}
return 0
}
func doesNotExist(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return true
}
return false
}
func CreateDirectoryIfNotExist(dirPath string) error {
if doesNotExist(dirPath) {
if err := os.Mkdir(dirPath, 0755); err != nil {
return err
}
}
return nil
}
type FileData struct {
config
}
func generateDefaultConfigFile(dirPath string, tmpl *template.Template, data FileData) error {
defaultFileName := "config.yaml"
filePath := filepath.Join(dirPath, defaultFileName)
if doesNotExist(filePath) {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
if err := tmpl.Execute(file, data); err != nil {
return err
}
}
return nil
}
And the config file would look like this:
no-tls-verify: true
username: username_via_config
password: password_via_config
host: http://localhost_via_config:8006/api2/json
verbose: true
Tags:
#go #cli