cli

package module
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Feb 11, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

CLI - Cobra Command Framework with Viper Configuration

A lightweight Go library that simplifies building CLI applications with Cobra commands and Viper configuration management. It provides sensible defaults for configuration loading, structured logging, and type-safe command execution.

Features

  • Type-safe command builders using Go generics for zero-boilerplate command setup
  • Automatic configuration loading from files, environment variables, and flags
  • Structured logging with zerolog and file rotation support
  • Context management for passing application metadata through command execution
  • Flexible command options for customizing command behavior

Installation

go get github.com/dioad/cli

Quick Start

Basic Command with Configuration
package main

import (
	"context"
	"fmt"
	"github.com/dioad/cli"
	"github.com/spf13/cobra"
)

type AppConfig struct {
	Name    string `mapstructure:"name"`
	Version string `mapstructure:"version"`
}

func main() {
	cfg := &AppConfig{
		Name:    "myapp",
		Version: "1.0.0",
	}

	cmd := &cobra.Command{
		Use:   "greet",
		Short: "Greet the user",
	}

	// Register command with type-safe config handling
	cli.NewCommand(cmd, greetCommand, cfg)

	err := cmd.Execute()
	if err != nil {
		panic(err)
	}
}

func greetCommand(ctx context.Context, cfg *AppConfig) error {
	fmt.Printf("Hello from %s v%s\n", cfg.Name, cfg.Version)
	return nil
}
With Configuration File

Configuration files are loaded from multiple locations in order of precedence:

  1. Path specified by --config flag
  2. Environment variables with prefix APPNAME_
  3. User config directory: $HOME/.config/{orgName}/{appName}/config.yaml
  4. System config: /etc/{orgName}/{appName}/config.yaml
  5. Current directory: ./config.yaml

Example config.yaml:

name: myapp
version: 1.0.0
log:
  level: debug
  file: "~/.myapp/logs/app.log"
  max-size: 100    # MB
  max-age: 7       # days
  max-backups: 3
  compress: true

Core Components

Command Builder

Create Cobra commands with type-safe configuration:

func NewCommand[T any](
	cmd *cobra.Command,
	runFunc func(context.Context, *T) error,
	defaultConfig *T,
	opts ...CommandOpt,
) *cobra.Command

Example:

type ServerConfig struct {
	Port int    `mapstructure:"port"`
	Host string `mapstructure:"host"`
}

cfg := &ServerConfig{Port: 8080, Host: "localhost"}

cmd := cli.NewCommand(
	&cobra.Command{
		Use:   "serve",
		Short: "Start the server",
	},
	func(ctx context.Context, cfg *ServerConfig) error {
		fmt.Printf("Starting server on %s:%d\n", cfg.Host, cfg.Port)
		return nil
	},
	cfg,
	cli.WithConfigFlag("config.yaml"),
)
Configuration Management
InitViperConfig

Initialize Viper with flag parsing and config loading:

type Config struct {
	Debug   bool   `mapstructure:"debug"`
	LogFile string `mapstructure:"log-file"`
}

cfg := &Config{}
err := cli.InitViperConfig("myorg", "myapp", cfg)
if err != nil {
	panic(err)
}
InitConfig

Load configuration from files and environment with Cobra command context:

func InitConfig(
	orgName, appName string,
	cmd *cobra.Command,
	cfgFile string,
	cfg interface{},
) (*CommonConfig, error)

Returns CommonConfig with logging configuration and populates the provided config struct.

Path Helpers

Get default paths for configuration and persistence:

// Get user's config directory
configPath, err := cli.DefaultConfigPath("myorg", "myapp")

// Get persistence directory (uses /persist in Docker)
persistPath, err := cli.DefaultPersistencePath("myorg", "myapp")

// Get full config file path
configFile, err := cli.DefaultConfigFile("myorg", "myapp", "config")
Context Management

Store and retrieve application metadata through context:

ctx := cli.Context(
	context.Background(),
	cli.SetOrgName("myorg"),
	cli.SetAppName("myapp"),
)

// Later, retrieve from context
orgName := cli.getOrgName(ctx)
appName := cli.getAppName(ctx)
Logging

The logging subpackage provides structured logging via zerolog:

import "github.com/dioad/cli/logging"
import "github.com/rs/zerolog/log"

// Configure logging from config
loggingCfg := logging.Config{
	Level:      "debug",
	File:       "~/.myapp/logs/app.log",
	MaxSize:    100,
	MaxBackups: 3,
	Compress:   true,
}

logging.ConfigureCmdLogger(loggingCfg)

// Use structured logging
log.Info().
	Str("user", "alice").
	Int("count", 42).
	Msg("operation completed")

Log Levels: trace, debug, info, warn, error, fatal, panic

Architecture Overview

Package cli
├── Command Building (NewCommand, CommandOpt)
├── Config Management (InitViperConfig, InitConfig)
├── Path Helpers (DefaultConfigPath, DefaultPersistencePath)
├── Context Management (SetOrgName, SetAppName)
└── Cobra Integration (CobraRunE, CobraRunEWithConfig)

Package logging
├── Configuration (Config struct)
├── Level Management (ConfigureLogLevel)
├── Output Management (ConfigureLogOutput, ConfigureLogFileOutput)
└── Logging Options (WithDefaultLogLevel)

Common Patterns

Multi-Level Commands
rootCmd := &cobra.Command{Use: "app"}
ctx := cli.Context(
	context.Background(),
	cli.SetOrgName("myorg"),
	cli.SetAppName("myapp"),
)
rootCmd.SetContext(ctx)

// Add subcommands
rootCmd.AddCommand(
	cli.NewCommand(
		&cobra.Command{Use: "start"},
		startCommand,
		&ServerConfig{},
		cli.WithConfigFlag("config.yaml"),
	),
)
Environment Variable Override

All configuration values can be overridden via environment variables using the format:

{APPNAME}_{SECTION}_{FIELD}

Example:

MYAPP_LOG_LEVEL=debug MYAPP_PORT=9000 ./myapp
Docker Support

The library automatically detects Docker environments:

if cli.IsDocker() {
	// Use /persist and /config directories
}

Configuration and persistence paths default to container volumes when running in Docker.

Examples

See the example/ directory for a complete working example demonstrating logging configuration and level testing.

Dependencies

License

See LICENSE file for details.

Documentation

Overview

Package cli provides utilities for building CLI applications with Cobra and Viper.

It simplifies the creation of command-line applications by integrating Cobra's command framework with Viper's configuration management, providing sensible defaults for:

- Configuration loading from files, environment variables, and command-line flags - Type-safe command execution using Go generics - Structured logging via zerolog - Path management with Docker support - Context propagation for application metadata

Basic Usage

Create a configuration struct:

type ServerConfig struct {
	Port int    `mapstructure:"port"`
	Host string `mapstructure:"host"`
}

Create a command handler:

func serveCommand(ctx context.Context, cfg *ServerConfig) error {
	fmt.Printf("Starting server on %s:%d\n", cfg.Host, cfg.Port)
	return nil
}

Register the command with NewCommand:

cfg := &ServerConfig{Port: 8080, Host: "localhost"}
cmd := cli.NewCommand(
	&cobra.Command{
		Use:   "serve",
		Short: "Start the server",
	},
	serveCommand,
	cfg,
	cli.WithConfigFlag("config.yaml"),
)

Configuration File Format

Configuration files are YAML with support for multiple sections:

port: 8080
host: localhost
log:
  level: debug
  file: "~/.app/logs/app.log"
  max-size: 100
  compress: true

Configuration Loading Order

Configurations are loaded and merged in this order (last wins):

1. Config file at /etc/{org}/{app}/config.yaml 2. Config file at $HOME/.config/{org}/{app}/config.yaml 3. Command-line flags 4. Environment variables with prefix {APPNAME}_ 5. Config file specified by --config flag

Context Management

Store application metadata in context for propagation through command execution:

ctx := cli.Context(
	context.Background(),
	cli.SetOrgName("myorg"),
	cli.SetAppName("myapp"),
)
cmd.SetContext(ctx)

Advanced Features

Path helpers detect and adapt to Docker environments:

configPath, err := cli.DefaultConfigPath("myorg", "myapp")
// Returns /config in Docker, $HOME/.config/myorg/myapp otherwise

Error handling patterns follow Go best practices with wrapped errors:

err := cli.InitViperConfig("org", "app", cfg)
if err != nil {
	return fmt.Errorf("initialization failed: %w", err)
}

See Also

Package logging: provides structured logging configuration via zerolog.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AppNameFromContext added in v0.7.0

func AppNameFromContext(ctx context.Context) string

AppNameFromContext retrieves the application name from the context.

func CobraRunE

func CobraRunE[T any](execFunc func(*T) error, opt ...CobraOpt[T]) func(cmd *cobra.Command, args []string) error

CobraRunE returns a Cobra RunE function with configuration management and functional options.

The returned function handles configuration initialization with org and app names from context, applies functional options to modify configuration, and passes configured values to the execution function.

func CobraRunEWithConfig

func CobraRunEWithConfig[T any](execFunc func(context.Context, *T) error, cfg *T) func(cmd *cobra.Command, args []string) error

CobraRunEWithConfig returns a Cobra RunE function that loads configuration before execution.

The returned function handles configuration loading, application metadata retrieval, and passes configured values to the execution function.

func Context

func Context(ctx context.Context, contextOpts ...ContextOpt) context.Context

Context creates a new context with optional application metadata.

It accepts functional options to populate the context with organization and app names.

Example

ExampleContext demonstrates creating a context with application metadata.

package main

import (
	"context"
	"fmt"

	"github.com/dioad/cli"
)

func main() {
	ctx := cli.Context(
		context.Background(),
		cli.SetOrgName("myorg"),
		cli.SetAppName("myapp"),
	)

	if ctx != nil {
		fmt.Println("Context created with metadata")
	}
}
Output:

Context created with metadata

func DefaultConfigFile

func DefaultConfigFile(orgName, appName, baseName string) (string, error)

DefaultConfigFile returns the full path to the default configuration file.

The file is placed in DefaultConfigPath and named {baseName}.yaml.

Example

ExampleDefaultConfigFile demonstrates getting a default config file path.

package main

import (
	"fmt"

	"github.com/dioad/cli"
)

func main() {
	file, err := cli.DefaultConfigFile("myorg", "myapp", "config")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	if file != "" {
		fmt.Println("Config file path obtained successfully")
	}
}
Output:

Config file path obtained successfully

func DefaultConfigPath

func DefaultConfigPath(orgName, appName string) (string, error)

DefaultConfigPath returns the default directory for configuration files.

For Docker containers, this returns /config. For other environments, it returns $HOME/.config/{orgName}/{appName}.

Example

ExampleDefaultConfigPath demonstrates getting the default config path.

package main

import (
	"fmt"

	"github.com/dioad/cli"
)

func main() {
	path, err := cli.DefaultConfigPath("myorg", "myapp")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	if path != "" {
		fmt.Println("Config path obtained successfully")
	}
}
Output:

Config path obtained successfully

func DefaultPersistenceFile

func DefaultPersistenceFile(orgName, appName, baseName string) (string, error)

DefaultPersistenceFile returns the full path to a persistence file.

The file is placed in DefaultPersistencePath and named {baseName}.yaml.

func DefaultPersistencePath

func DefaultPersistencePath(orgName, appName string) (string, error)

DefaultPersistencePath returns the default directory for persistent application data.

For Docker containers, this returns /persist. For other environments, it returns the same as DefaultUserConfigPath.

Example

ExampleDefaultPersistencePath demonstrates getting the persistence path.

package main

import (
	"fmt"

	"github.com/dioad/cli"
)

func main() {
	path, err := cli.DefaultPersistencePath("myorg", "myapp")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	if path != "" {
		fmt.Println("Persistence path obtained successfully")
	}
}
Output:

Persistence path obtained successfully

func DefaultUserConfigPath

func DefaultUserConfigPath(orgName, appName string) (string, error)

DefaultUserConfigPath returns the default configuration directory for the user.

For non-Docker environments, it returns $HOME/.config/{orgName}/{appName} and creates the directory if it doesn't exist with 0700 permissions.

func InitViperConfig

func InitViperConfig(orgName, appName string, cfg interface{}) error

InitViperConfig initializes Viper configuration management with parsed command-line flags.

It sets up Viper to: - Bind command-line flags - Search for configuration files in standard locations - Support environment variables with the given appName prefix - Unmarshal configuration into the provided cfg struct

Configuration sources are merged with this precedence (highest to lowest): 1. Command-line flags 2. Explicit config file (--config flag) 3. Environment variables (prefixed with appName) 4. Config files in standard locations

func InitViperConfigWithFlagSet

func InitViperConfigWithFlagSet(orgName, appName string, cfg interface{}, parsedFlagSet *pflag.FlagSet) error

InitViperConfigWithFlagSet initializes Viper with a custom FlagSet.

Similar to InitViperConfig but allows specifying a custom pflag.FlagSet instead of using the global command line flags. Useful for embedding configuration initialization in library code or tests.

func IsDocker

func IsDocker() bool

IsDocker detects if the application is running inside a Docker container.

It checks for the presence of /.dockerenv file, which is a standard indicator that the process is running in a Docker container.

Example

ExampleIsDocker demonstrates Docker detection.

package main

import (
	"fmt"

	"github.com/dioad/cli"
)

func main() {
	inDocker := cli.IsDocker()
	if inDocker {
		fmt.Println("Running in Docker")
	} else {
		fmt.Println("Running on host machine")
	}
}
Output:

Running on host machine

func NewCommand

func NewCommand[T any](cmd *cobra.Command, runFunc func(context.Context, *T) error, defaultConfig *T, opts ...CommandOpt) *cobra.Command

NewCommand creates a new Cobra command with type-safe configuration handling.

It wraps the provided Cobra command with automatic configuration loading, populating flags from the config struct, and executing the command with configuration management.

Example

ExampleNewCommand demonstrates creating a type-safe command with configuration.

package main

import (
	"context"
	"fmt"

	"github.com/dioad/cli"
	"github.com/spf13/cobra"
)

func main() {
	type Config struct {
		Name string `mapstructure:"name"`
		Port int    `mapstructure:"port"`
	}

	cfg := &Config{
		Name: "example-service",
		Port: 8080,
	}

	cmd := cli.NewCommand(
		&cobra.Command{
			Use:   "serve",
			Short: "Start the service",
		},
		func(ctx context.Context, c *Config) error {
			fmt.Printf("Service %s listening on port %d\n", c.Name, c.Port)
			return nil
		},
		cfg,
		cli.WithConfigFlag("config.yaml"),
	)

	if cmd != nil {
		fmt.Println("Command created successfully")
	}
}
Output:

Command created successfully

func OrgNameFromContext added in v0.7.0

func OrgNameFromContext(ctx context.Context) string

OrgNameFromContext retrieves the organization name from the context.

func UnmarshalConfig added in v0.7.0

func UnmarshalConfig(v *viper.Viper, c interface{}) error

UnmarshalConfig unmarshals Viper configuration into the provided struct with custom decode hooks.

It uses a composed decode hook to handle special types like MaskedString, time.Duration, net.IP, and net.IPNet. This allows for seamless unmarshalling of complex configuration fields.

func ValidateName added in v0.7.0

func ValidateName(name string) error

ValidateName checks that the provided name is valid for use in configuration paths.

It ensures that the name is not empty, does not contain path separators, does not start or end with spaces, and does not contain special characters. This validation helps prevent directory traversal issues and ensures clean config paths.

func ValidateOrgAndAppName added in v0.7.0

func ValidateOrgAndAppName(orgName, appName string) error

ValidateOrgAndAppName validates that orgName and appName are acceptable names.

It delegates to ValidateName, which ensures that names are non-empty, do not contain path separators, spaces (including leading or trailing spaces), or various special characters. This helps prevent issues when constructing configuration paths and other filesystem-related operations.

func WithConfigFlag

func WithConfigFlag(defaultConfigFile string) func(*cobra.Command)

WithConfigFlag adds a --config/-c flag to the command for specifying a config file path.

Example

ExampleWithConfigFlag demonstrates adding a config file flag to a command.

package main

import (
	"fmt"

	"github.com/dioad/cli"
	"github.com/spf13/cobra"
)

func main() {
	cmd := &cobra.Command{Use: "serve"}
	opt := cli.WithConfigFlag("~/.app/config.yaml")
	opt(cmd)

	configFlag := cmd.Flag("config")
	if configFlag != nil {
		fmt.Printf("Config flag default: %s\n", configFlag.DefValue)
	}
}
Output:

Config flag default: ~/.app/config.yaml

Types

type CobraOpt

type CobraOpt[T any] func(*T)

CobraOpt is a functional option for configuring command execution.

type CommandOpt

type CommandOpt func(*cobra.Command)

CommandOpt is a functional option for customizing a Cobra command.

type CommonConfig

type CommonConfig struct {
	// Config  string         `mapstructure:"config"`
	Logging logging.Config `mapstructure:"log"`
}

CommonConfig contains configuration shared across all applications.

It includes logging configuration and can be extended in application-specific config structs via embedding.

func InitConfig

func InitConfig(orgName, appName string, cmd *cobra.Command, cfgFile string, cfg interface{}) (*CommonConfig, error)

InitConfig loads and initializes configuration from multiple sources.

It integrates Cobra commands with Viper configuration management, supporting: - Hierarchical command-based config file naming - Flag binding from the Cobra command - Environment variable overrides - Automatic logging configuration - Configuration hot-reloading via Viper watchers

type Config

type Config[T any] struct {
	CommonConfig
	Config *T
}

type ContextOpt

type ContextOpt func(context.Context) context.Context

ContextOpt is a functional option for building a context with application metadata.

func SetAppName

func SetAppName(appName string) ContextOpt

SetAppName returns a ContextOpt that stores the application name in the context.

Example

ExampleSetAppName demonstrates setting app name in context.

package main

import (
	"context"
	"fmt"

	"github.com/dioad/cli"
)

func main() {
	opt := cli.SetAppName("myservice")
	ctx := opt(context.Background())

	if ctx != nil {
		fmt.Println("App name set in context")
	}
}
Output:

App name set in context

func SetOrgName

func SetOrgName(orgName string) ContextOpt

SetOrgName returns a ContextOpt that stores the organization name in the context.

Example

ExampleSetOrgName demonstrates setting organization name in context.

package main

import (
	"context"
	"fmt"

	"github.com/dioad/cli"
)

func main() {
	opt := cli.SetOrgName("acme-corp")
	ctx := opt(context.Background())

	if ctx != nil {
		fmt.Println("Organization name set in context")
	}
}
Output:

Organization name set in context

Directories

Path Synopsis
Package logging provides structured logging configuration and management.
Package logging provides structured logging configuration and management.

Jump to

Keyboard shortcuts

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