nicecmd

package module
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2025 License: Apache-2.0 Imports: 15 Imported by: 1

README

nicecmd

Cobra gives you nice CLIs already, and this package adds nice bindings to your variables on top. It is for you if you want Cobra, but programming with Cobra as-is seems just a bit too verbose for your taste.

  • Define your config and defaults with structs
  • This package uses reflection to pre-populate your cobra.Command with flags
  • Environment variable and dotenv-style config file support is automatic
package main

import (
	"github.com/mologie/nicecmd"
	"github.com/spf13/cobra"
	"os"
)

type Config struct {
	Name    string `flag:"required" usage:"person to greet"`
	Weather string `param:"w" usage:"how's the weather?"`
}

func main() {
	cmd := nicecmd.RootCommand(nicecmd.Run(greet), cobra.Command{
		Use:   "nicecmd-readme --name <name> [-w <weather>]",
		Short: "It's just Cobra, but with no binding/setup required!",
	}, Config{
		Weather: "nice",
	})
	if err := cmd.Execute(); err != nil {
		os.Exit(1)
	}
}

func greet(cfg *Config, cmd *cobra.Command, args []string) error {
	cmd.Printf("Hello, %s!\n", cfg.Name)
	cmd.Printf("The weather looks %s today!\n", cfg.Weather)
	return nil
}
$ go run ./cmd/nicecmd-readme --help
It's just Cobra, but with no binding/setup required!

Usage:
  nicecmd-readme --name <name> [-w <weather>]
  nicecmd-readme [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  printenv    Print all environment variable values or defaults for this command

Flags:
      --name string            person to greet (env NICECMD_README_NAME) (required)
  -w, --weather string         how's the weather? (env NICECMD_README_WEATHER) (default "nice")
      --env-file stringArray   load dotenv file (repeat for multiple files)
      --env-overwrite          give precedence to dotenv environment variables
      --env-lax                ignore unbound environment variables
  -h, --help                   help for nicecmd-readme

Use "nicecmd-readme [command] --help" for more information about a command.

A more complete example with a sub-command is available in cmd/nicecmd-fizzbuzz. Additionally, reflect_test.go and the documentation of Cobra and pflag will be useful for more complex CLI tools.

Principles and Patterns

Immutable, type-safe, private configuration

All configuration is passed to commands as struct. Default values are encoded in a type-safe way in the initial struct passed to nicecmd.RootCommand. Only the command's run function gets access to the filled-out configuration.

Avoid global variables

Cobra's examples suggest to have all commands in one package, and use Go's init() function to register the command with some global rootCmd. I found that this quickly created clashes between unrelated state of various subcommands. Likewise, you could access another command's variables, despite them being uninitialized.

With nicecmd all configuration is in a struct, and you get a private copy of it to work with. This avoids global variables for parameters.

Sub-commands

nicecmd.SubCommand will prefix the command's env vars with the parent command's path.

You should structure sub-commands so that any shared configuration is a local (or persistent for convenience) variable on the parent command. For example, a log level would be shared for the entire application.

You however cannot access the configuration of a parent command in the sub-command! Instead, modify the command context from the setup hook, e.g. to inject a logger:

type RootConfig struct { LogLevel string }
type SubConfig struct {}

rootCmd := nicecmd.RootCommand(nicecmd.Setup(setup), cobra.Command{
	Use:   "foo [--log-level <level>] <command>"
	Short: "Foo will fizz your buzz"
}, RootConfig{})

nicecmd.SubCommand(rootCmd, nicecmd.Run(run), cobra.Command{
	Use:   "bar"
	Short: "Do the fizzing and buzzing"
}, SubConfig{})

func setup(cfg *RootConfig, cmd *cobra.Command, args []string) error {
	// This always gets called before bar (or any other sub-command).
	myLog := logutil.NewSLog(cfg.LogLevel)
	cmd.SetContext(logutil.WithLogContext(cmd.Context(), myLog))
}

func run(cfg *SubConfig, cmd *cobra.Command, args []string) error {
	log := logutil.FromContext(cmd.Context())
	log.Debug("fizz buzzing will commence") // but is omitted
}

This pattern should apply to pretty much any kind of state that you need to create and inherit to sub-commands. If you need an escape hatch, you can still update the context with a pointer to the entire RootConfig struct and let your sub-command do the setup regardless.

Required parameters

Use flag:"required" to mark a flag as required. This is preferred over checking for absent values in code, because Cobra will aggregate errors and display all missing flags to the user for you.

Persistent parameters

Cobra has a concept of persistent parameters. A flag can be made persistent via flag:"persistent". This tag is comma-separated, e.g. flag:"required,persistent" is valid.

To illustrate with an example an optional persistent --log-level on the root command would make both of these invocations valid:

  • foo --log-level=debug server
  • foo server --log-level=debug

Whereas if log-level was not persistent, only the first command would work.

Automatic naming

This package will automatically derive a name for parameters and environment variables from the field name of your configuration structure. A prefix is derived from the first word of a command's Use line. Individual field names can be overridden via param and env:

  • FooBarBaz string is set via param --foo-bar-baz or env var FOO_BAR_BAZ
  • Use param:"foo" to change just the long form
  • Use param:"foo,f" to change the long form and add a short form
  • Use param:"f" to add a short form and keep the default long name
  • Every parameter must have a long form, I find that more intuitive.
  • Use env:"FOO" to define a custom environment variable to read from. No prefix will be added!
  • Use env:"-" to remove the environment variable. Useful for flags like --version.
Sub-structs are flattened with a prefix

Take the following example, where Config is used for some nicecmd.Command:

type LogConfig struct {
	Level  int    `usage:"raise the bar"`
	Format string `usage:"TEXT or JSON"`
}

type Config struct {
	Log LogConfig `flag:"persistent"`
}
  • This gets you the parameters --log-level and --log-format.
  • Using param on Log would change the prefix.
  • Flag options are inherited: The whole struct becomes persistent.
Configuration files

Viper (from the authors of Cobra) is a pretty nice configuration library, but comes with a bunch of dependencies. NiceCmd does not care about configuration files: It gives you environment variables, which is usually sufficient for configuring containerized applications.

Append printenv to a command to dump its configuration as dotenv file.

If you need more, you can set nicecmd.Environment = false and let Viper handle everything.

License

NiceCmd is released under the Apache 2.0 license.

Contributions

I welcome contributions to this project, but may reject contributions that don't fit its spirit:

  1. This is a rather opinionated library built on top of Cobra. It does not follow Cobra's defaults and conventions. If you need to customize something, then interacting with Cobra or changing its settings directly is usually the way to go.
  2. The library should remain minimal. I will reject contributions that add dependencies. (The stdlib is mostly fine.)
  3. I would treat this project like go fmt: If something looks off or is awkward to use, then that's a bug too. NiceCmd should after all make your command line code nice.
  4. Contributions that come with code must come with tests.
  5. Contributions must be licensed under the Apache 2.0 license.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Environment = true

Environment determines whether environment variables are bound and processed. Change this globally if you use another library for environment variables, e.g. Viper.

Functions

func BindConfig

func BindConfig(cmd *cobra.Command, cfg any, opts ...Option)

BindConfig maps fields of cfg to flag sets of cmd. A field's value is set with the following precedence: Explicit flag, environment variable, then whatever is already set in cfg.

Struct tags:

  • flag: Set of the flags defined above, separated by commas.
  • param: "foo,f" for --foo=bar or -f x. Defaults to kebab-case of field name, long opt only.
  • encoding: Type-specific encoding, e.g. "base64" for []byte.
  • env: Environment variable name, "-" for none, defaults to prefixed screaming snake case.
  • usage: Flag usage string. Environment variable name is appended if set.

func RegisterType added in v0.2.0

func RegisterType[T any](
	parse func(value string) (T, error),
	serialize func(value T) string,
)

RegisterType registers a custom type for BindConfig. The typical use-case for RegisterType is to make third-party types embeddable into configuration structs. Implementing pflag.Value is the nicer solution for first-party types, because it does not rely on global state.

func RootCommand added in v0.2.0

func RootCommand[T any](hooks Hooks[T], cmd cobra.Command, cfg T, opts ...Option) *cobra.Command

RootCommand adds nicecmd-specific persistent arguments to the given command, e.g. --env-file for loading dotenv files. It is otherwise identical to Command.

func RootGroup added in v0.2.1

func RootGroup(cmdTmpl cobra.Command, sub ...func(*cobra.Command)) *cobra.Command

RootGroup is a convenience function to construct a command group at the application's root. The command group does not take any special configuration.

func SubCommand added in v0.2.0

func SubCommand[T any](parent *cobra.Command, hooks Hooks[T], cmd cobra.Command, cfg T, opts ...Option) *cobra.Command

SubCommand wraps a cobra.Command to pass a config bound via BindConfig to a set of cobra run functions.

func SubGroup added in v0.2.1

func SubGroup(parent *cobra.Command, cmdTmpl cobra.Command, sub ...func(*cobra.Command)) *cobra.Command

SubGroup is a convenience function to construct a command group without special configuration.

func UnregisterType added in v0.2.0

func UnregisterType[T any]()

UnregisterType removes a custom type registration. Useful for testing.

Types

type ErrInvalidEnvironment added in v0.2.0

type ErrInvalidEnvironment struct {
	FlagErrors []FlagError
}

func (ErrInvalidEnvironment) Error added in v0.2.0

func (e ErrInvalidEnvironment) Error() string

type ErrUnboundEnvironment added in v0.2.0

type ErrUnboundEnvironment struct {
	Names []string
}

func (ErrUnboundEnvironment) Error added in v0.2.0

func (e ErrUnboundEnvironment) Error() string

type FlagError added in v0.2.0

type FlagError struct {
	Flag  *pflag.Flag
	Error error
}

type Hook added in v0.2.0

type Hook[T any] func(cfg *T, cmd *cobra.Command, args []string) error

Hook matches cobra.Command's various RunE functions, plus a config to be bound via Command.

type Hooks added in v0.2.0

type Hooks[T any] struct {
	PersistentPreRun  Hook[T]
	PreRun            Hook[T]
	Run               Hook[T]
	PostRun           Hook[T]
	PersistentPostRun Hook[T]
}

Hooks provides various RunE functions of cobra.Command. Cobra's naming scheme is reused here. For clarity, only the "persistent" hooks run even when a subcommand is called.

func Run

func Run[T any](f Hook[T]) Hooks[T]

Run is a convenience function to create Hooks with only the Run function set. This function is executed only when the user runs this specific command.

func Setup added in v0.2.0

func Setup[T any](f Hook[T]) Hooks[T]

Setup is a convenience function to create Hooks with only the PersistentPreRun function set. This function is executed for sub-commands too, but before argument validation.

func SetupAndRun added in v0.2.0

func SetupAndRun[T any](setup Hook[T], run Hook[T]) Hooks[T]

SetupAndRun is a convenience function that combines Setup and Run.

type Option added in v0.2.0

type Option func(*config)

func WithEnvPrefix added in v0.2.0

func WithEnvPrefix(prefix string) Option

WithEnvPrefix sets a prefix to prepend to env vars, separated by an underscore. For sub-structs, the prefix is further extended with the screaming snake case of the field name under which the struct is embedded. When no prefix is set, then environment variables are unavailable unless set explicitly.

Directories

Path Synopsis
cmd
nicecmd-fizzbuzz command
nicecmd-fizzbuzz is an enterprise-grade, highly configurable fizz buzzer (everything in this tool is nonsense, but should demonstrate nicecmd usage)
nicecmd-fizzbuzz is an enterprise-grade, highly configurable fizz buzzer (everything in this tool is nonsense, but should demonstrate nicecmd usage)
nicecmd-readme command

Jump to

Keyboard shortcuts

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