cli

package module
v1.6.1 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2025 License: MIT Imports: 8 Imported by: 4

README

License go.mod Go version GoDoc Latest tag Go Report

cli: Command Line Interface Made Simpler

The cli package provides a framework-agnostic way to define command-line interfaces in Go. Its primary goal is to decouple your application's CLI logic from specific CLI frameworks like spf13/cobra or urfave/cli. This allows for easier testing, greater flexibility, and the ability to switch between underlying CLI frameworks without significant code changes.

Motivation

Existing CLI frameworks, while powerful, often lead to tight coupling. Changing frameworks can require extensive rewrites.

cli aims to solve this by:

  • Abstraction: Providing a simple, consistent interface for defining commands.
  • Testability: Making it easy to test command logic in isolation, without framework dependencies.
  • Flexibility: Allowing you to choose (and change) the underlying CLI framework that best suits your needs.
  • Simplicity: Focusing on core CLI functionality, avoiding unnecessary complexity.

API Stability

This project follows Semantic Versioning.

Usage

Basic Command

The simplest command implements the Command interface:

package main

import (
    "context"
    "os"

    "github.com/krostar/cli"
    spf13cobra "github.com/krostar/cli/mapper/spf13/cobra"
)

type myCommand struct{}

func (myCommand) Execute(ctx context.Context, args []string, dashedArgs []string) error {
    // Your command logic here.
    // args: Positional arguments.
    // dashedArgs: Arguments after a "--".
    return nil
}

func main() {
    cmd := cli.New(myCommand{})
    err := spf13cobra.Execute(context.Background(), os.Args, cmd)
    cli.Exit(context.Background(), err)
}
Adding Subcommands
type subCommand struct{}

func (subCommand) Execute(ctx context.Context, args []string, dashedArgs []string) error {
    // Subcommand logic.
    return nil
}

func main() {
    cmd := cli.New(myCommand{}).AddCommand("sub", subCommand{}) // Add a subcommand named "sub".

    // Or, mount a complete CLI as a subcommand:
    subCLI := cli.New(subCommand{})
    cmd.Mount("another", subCLI)

    err := spf13cobra.Execute(context.Background(), os.Args, cmd)
    cli.Exit(context.Background(), err)
}
Flags
type flagCommand struct {
    name string
    age  int
    tags []string
}

func (c *flagCommand) Flags() []cli.Flag {
    return []cli.Flag{
        cli.NewBuiltinFlag("name", "", &c.name, "Your name"),
        cli.NewBuiltinFlag("age", "", &c.age, "Your age"),
        cli.NewBuiltinSliceFlag("tags", "t", &c.tags, "Comma-separated tags"),
    }
}

func (c *flagCommand) Execute(ctx context.Context, args []string, dashedArgs []string) error {
    // Access flag values: c.name, c.age, c.tags
    return nil
}
Hooks
type hookedCommand struct{}

func (hookedCommand) Execute(ctx context.Context, args []string, dashedArgs []string) error {
    // command logic
    return nil
}

func (c hookedCommand) Hook() *cli.Hook {
    return &cli.Hook{
        BeforeCommandExecution: func(ctx context.Context) error {
            // Code to run before Execute.
            return nil
        },
        AfterCommandExecution: func(ctx context.Context) error {
            // Code to run after Execute.
            return nil
        },
    }
}
Signal Handling
func main() {
    ctx, cancel := cli.NewContextCancelableBySignal(syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

    err := spf13cobra.Execute(ctx, os.Args, /* ... */)
    cli.Exit(ctx, err)
}

Configuration Management

The cli package provides a powerful configuration system through the cfg package that allows loading configuration from multiple sources with precedence.

Configuration Sources

The following sources are supported out of the box:

  • Default Values: Set default values for your configuration
  • Environment Variables: Load configuration from environment variables
  • Configuration Files: Load configuration from YAML/JSON files
  • Command-line Flags: Load configuration from command-line flags
Configuration Example
import (
    "github.com/krostar/cli"
    clicfg "github.com/krostar/cli/cfg"
    "github.com/krostar/cli/cfg/source/default"
    "github.com/krostar/cli/cfg/source/env"
    "github.com/krostar/cli/cfg/source/file"
    "github.com/krostar/cli/cfg/source/flag"
)

// Config structure with environment variable mappings
type Config struct {
    Server struct {
        Host string `env:"SERVER_HOST"`
        Port int    `env:"SERVER_PORT"`
    }
    LogLevel   string `env:"LOG_LEVEL"`
    ConfigFile string `env:"CONFIG_FILE"`
}

// SetDefault implements the default values
func (cfg *Config) SetDefault() {
    cfg.Server.Host = "localhost"
    cfg.Server.Port = 8080
    cfg.LogLevel = "info"
    cfg.ConfigFile = "config.yaml"
}

// Command with configuration
type MyCommand struct {
    config Config
}

// Define flags that map to your configuration
func (cmd *MyCommand) Flags() []cli.Flag {
    return []cli.Flag{
        cli.NewBuiltinFlag("config", "c", &cmd.config.ConfigFile, "Path to config file"),
        cli.NewBuiltinFlag("host", "", &cmd.config.Server.Host, "Server host"),
        cli.NewBuiltinFlag("port", "p", &cmd.config.Server.Port, "Server port"),
        cli.NewBuiltinFlag("log-level", "l", &cmd.config.LogLevel, "Log level"),
    }
}

// Use hooks to load configuration in order of precedence
func (cmd *MyCommand) Hook() *cli.Hook {
    return &cli.Hook{
        BeforeCommandExecution: clicfg.BeforeCommandExecutionHook(
            &cmd.config,
            // Sources are applied in order, with later sources taking precedence
            sourcedefault.Source[Config](),                              // 1. Defaults
            sourcefile.Source(getConfigFilePath, yamlUnmarshaler, true), // 2. Config file
            sourceenv.Source[Config]("APP"),                             // 3. Environment variables
            sourceflag.Source[Config](cmd),                              // 4. Command-line flags
        ),
    }
}

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Overview

Package cli provides a framework-agnostic way to define command-line interfaces in Go.

The core philosophy of this package is to decouple your application's command-line interface logic from specific CLI frameworks like spf13/cobra or urfave/cli. This approach offers several benefits:

  1. Testability: Commands and their logic can be easily tested in isolation, without framework dependencies

  2. Flexibility: You can switch between underlying CLI frameworks without significant code changes

  3. Simplicity: The package focuses on core CLI functionality, avoiding unnecessary complexity

  4. Extensibility: Support for custom hooks, flags, and configuration sources

At its core, the package uses a simple Command interface that all CLI commands implement:

type Command interface {
	Execute(ctx context.Context, args, dashedArgs []string) error
}

Additional interfaces like CommandFlags, CommandDescription, CommandHook, etc., can be implemented to add functionality as needed.

The package also includes a robust configuration system through the cfg subpackage, allowing commands to load configuration from multiple sources (environment variables, files, command-line flags) with a clear precedence order.

Basic usage:

cmd := cli.New(rootCommand{}).
    AddCommand("sub", subCommand{})

err := spf13cobra.Execute(context.Background(), os.Args, cmd)
cli.Exit(context.Background(), err)

For detailed examples, see the README and the example package.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Exit

func Exit(ctx context.Context, err error, options ...ExitOption)

Exit terminates the CLI application, handling errors and setting the appropriate exit status code.

func GetInitializedFlagsFromContext added in v1.2.0

func GetInitializedFlagsFromContext(ctx context.Context) ([]Flag, []Flag)

GetInitializedFlagsFromContext retrieves the initialized local and persistent flags for a command from the context. Returns nil slices if no flags are found. Warning: This function is exposed for sourcing flag values in configuration loader, you should not use it directly.

func GetMetadataFromContext

func GetMetadataFromContext(ctx context.Context, key any) any

GetMetadataFromContext retrieves a value from the global CLI metadata store based on the provided key. Returns nil if the key is not found.

func NewCommandContext added in v1.2.0

func NewCommandContext(ctx context.Context) context.Context

NewCommandContext is called for each command to create a dedicated context. Warning: This does not make sens to use outside of cli mapper.

func NewContextCancelableBySignal

func NewContextCancelableBySignal(sig os.Signal, sigs ...os.Signal) (context.Context, func())

NewContextCancelableBySignal creates a new context that is automatically canceled when any of the provided signals are received. This is useful for gracefully shutting down the CLI application on interrupt signals (e.g., Ctrl+C).

func NewContextWithMetadata

func NewContextWithMetadata(ctx context.Context) context.Context

NewContextWithMetadata creates a new context that includes a metadata store. This store can be used to pass arbitrary data between different parts of the CLI application. This function should be called at the beginning of the CLI application's execution.

func NewErrorWithExitStatus

func NewErrorWithExitStatus(err error, status uint8) error

NewErrorWithExitStatus creates a new error that implements the ExitStatusError interface, allowing a custom exit status to be set.

func NewErrorWithHelp

func NewErrorWithHelp(err error) error

NewErrorWithHelp creates a new error that implements the ShowHelpError interface, indicating that the help message should be shown.

func SetExitLoggerInMetadata

func SetExitLoggerInMetadata(ctx context.Context, writer io.WriteCloser)

SetExitLoggerInMetadata sets the logger used by the CLI to write the exit message if any, inside the metadata. By default, the Exit func tries to find the logger in the metadata.

func SetInitializedFlagsInContext added in v1.2.0

func SetInitializedFlagsInContext(ctx context.Context, localFlags, persistentFlags []Flag)

SetInitializedFlagsInContext stores the initialized local and persistent flags for a command in the context. This allows the flags to be accessed later, for example, by configuration sources. Warning: This function is exposed for cli mappers, you should not use it directly.

func SetMetadataInContext

func SetMetadataInContext(ctx context.Context, key, value any)

SetMetadataInContext associates a key-value pair in the global CLI metadata store. This allows for storing and retrieving data that needs to be accessible across the entire CLI application.

Types

type CLI

type CLI struct {
	// Name is the name of the command as it appears on the command line.
	// For the root command, this is typically empty as it represents the application itself.
	// For subcommands, it's the command name that will be used to invoke it.
	Name string

	// Command is the command to execute for this CLI.
	// It must implement at least the Command interface, and may optionally
	// implement additional interfaces like CommandFlags, CommandHook, etc.
	Command Command

	// SubCommands is a list of subcommands for this CLI.
	// These are commands that can be invoked under the parent command.
	SubCommands []*CLI
}

CLI represents a command-line interface with a root command and optional subcommands. It provides the structure for building hierarchical CLI applications.

func New added in v1.3.0

func New(cmd Command) *CLI

New creates a new CLI with the given command as its root. This is the entry point for building a CLI application.

Example:

rootCmd := New(myRootCommand{})

func (*CLI) AddCommand

func (cli *CLI) AddCommand(name string, cmd Command) *CLI

AddCommand adds a subcommand to the CLI with the given name. The name will be used to invoke the command on the command line. Returns the CLI instance for chaining method calls.

Example:

rootCmd := New(myRootCommand{}).
    AddCommand("serve", &serveCommand{}).
    AddCommand("version", &versionCommand{})

func (*CLI) Mount added in v1.1.0

func (cli *CLI) Mount(name string, sub *CLI) *CLI

Mount adds a pre-configured CLI hierarchy as a subcommand to the current CLI. This allows for composing complex CLI structures from simpler ones. Unlike AddCommand which adds a single command, Mount adds an entire command tree with its own subcommands.

The name parameter sets the name of the mounted CLI's root command. Returns the current CLI instance for chaining method calls.

Example:

// Create a sub-CLI
userCLI := New(userRootCommand{}).
    AddCommand("list", &listUsersCommand{}).
    AddCommand("create", &createUserCommand{})

// Mount it to the main CLI
mainCLI := New(mainRootCommand{}).
    Mount("user", userCLI)

type Command

type Command interface {
	Execute(ctx context.Context, args, dashedArgs []string) error
}

Command is the fundamental interface for all CLI commands.

type CommandContext

type CommandContext interface {
	Context(ctx context.Context) context.Context
}

CommandContext allows commands to customize the context passed to their subcommands. This is useful for propagating configuration, dependencies, or other context-specific data down the command tree.

type CommandDescription

type CommandDescription interface{ Description() string }

CommandDescription allows a command to provide a human-readable description of its purpose. This is used for generating help text. A short description is created from the first description line.

type CommandExamples

type CommandExamples interface{ Examples() []string }

CommandExamples allows a command to provide usage examples. These examples are displayed in the help text to guide users on how to use the command.

type CommandFlags

type CommandFlags interface{ Flags() []Flag }

CommandFlags allows a command to define command-line flags.

type CommandHook

type CommandHook interface{ Hook() *Hook }

CommandHook allows a command to define callbacks (hooks) that are executed at specific points in the command's lifecycle. This enables custom behavior before or after command execution.

type CommandPersistentFlags

type CommandPersistentFlags interface{ PersistentFlags() []Flag }

CommandPersistentFlags allows a command to define flags that` are inherited by all of its subcommands.

type CommandPersistentHook

type CommandPersistentHook interface{ PersistentHook() *PersistentHook }

CommandPersistentHook allows a command to define persistent hooks that are executed for the command and all of its subcommands.

type CommandUsage

type CommandUsage interface{ Usage() string }

CommandUsage allows a command to specify its argument usage pattern. This helps define how arguments should be passed to the command.

type ExitOption

type ExitOption func(*exitOptions)

ExitOption defines the function signature for options that can be passed to the Exit function.

func WithExitFunc

func WithExitFunc(exitFunc func(status int)) ExitOption

WithExitFunc allows overriding the default exit function (os.Exit) with a custom function. This is primarily useful for testing.

func WithExitLoggerFunc

func WithExitLoggerFunc(getLoggerFunc func(context.Context) io.WriteCloser) ExitOption

WithExitLoggerFunc defines a way to customize logger used in messages.

type ExitStatusError

type ExitStatusError interface {
	error
	ExitStatus() uint8
}

ExitStatusError allows commands to specify a custom exit status code for the CLI application.

type Flag

type Flag interface {
	// FlagValuer defines methods to get and set flag's value.
	FlagValuer

	// LongName returns the long name of the flag (e.g., "--verbose").
	LongName() string
	// ShortName returns the short name of the flag (e.g., "-v"). May be empty.
	ShortName() string
	// Description returns a description of the flag's purpose.
	Description() string
}

Flag represents a command-line flag. It combines the FlagValuer interface with methods to access flag metadata (long name, short name, description).

func NewBuiltinFlag added in v1.2.0

func NewBuiltinFlag[T builtins](longName, shortName string, destination *T, description string) Flag

NewBuiltinFlag creates a Flag for built-in types (int, string, bool, etc.). It handles the conversion between string representations and the underlying Go type. See NewFlag for more details.

func NewBuiltinPointerFlag added in v1.2.0

func NewBuiltinPointerFlag[T builtins](longName, shortName string, destination **T, description string) Flag

NewBuiltinPointerFlag creates a Flag for pointers to built-in types. See NewBuiltinFlag for more details.

func NewBuiltinSliceFlag added in v1.2.0

func NewBuiltinSliceFlag[T builtins](longName, shortName string, destination *[]T, description string) Flag

NewBuiltinSliceFlag creates a Flag for slices of built-in types. The flag value is expected to be a comma-separated list of values. See NewBuiltinFlag for more details.

func NewFlag

func NewFlag(longName, shortName string, valuer FlagValuer, description string) Flag

NewFlag creates a new Flag instance.

longName is the long flag name, like --longname ; cannot be empty.
shortName is the short flag name ; usually 1 character, like -s ; can be empty.
valuer provide the way to set value to the destination.
description is a short text explaining the flag ; can be empty.

It panics if invalid inputs are provided.

type FlagValuer

type FlagValuer interface {
	// Destination returns a pointer to the variable that stores the flag's value.
	// This allows CLI frameworks to access the underlying value directly.
	Destination() any

	// FromString parses a string and sets the flag's value.
	// This is called when a flag is specified on the command line.
	// It should convert the string representation to the appropriate type
	// and store it in the destination.
	FromString(str string) error

	// IsSet returns true if the flag has been set (i.e., its value has been
	// modified from the default). This allows commands to determine if a
	// flag was explicitly provided by the user.
	IsSet() bool

	// String returns a string representation of the flag's current value.
	// This is used for displaying the flag's value in help text and error messages.
	String() string

	// TypeRepr returns a string representing the underlying type of the
	// flag's value (e.g., "int", "string", "bool"). This is used in help text
	// to indicate the expected type of the flag's value.
	TypeRepr() string
}

FlagValuer defines the interface for getting and setting the value of a flag. It abstracts the underlying type of the flag, allowing for type-safe flag handling while providing a common interface for CLI frameworks to interact with.

By implementing this interface, you can create custom flag types that work seamlessly with the CLI framework.

func NewFlagValuer added in v1.2.0

func NewFlagValuer[T any](destination *T, parse func(string) (T, error), toString func(T) string) FlagValuer

NewFlagValuer creates a new FlagValuer instance for any type. It provides a generic implementation of the FlagValuer interface that can work with any Go type.

Parameters:

  • destination: Pointer to where the flag value will be stored
  • parse: Function to parse a string into the destination type
  • toString: Function to convert the destination type to a string

Returns a FlagValuer that manages the value at destination.

It panics if any of the parameters is nil.

Example:

var count int
valuer := NewFlagValuer(
    &count,
    func(s string) (int, error) { return strconv.Atoi(s) },
    func(i int) string { return strconv.Itoa(i) },
)

func NewStringerFlagValuer added in v1.2.0

func NewStringerFlagValuer[T fmt.Stringer](destination *T, parse func(string) (T, error)) FlagValuer

NewStringerFlagValuer creates a FlagValuer for types that implement the fmt.Stringer interface. It simplifies the creation of FlagValuers for types that already have a String() method.

Parameters:

  • destination: Pointer to where the flag value will be stored
  • parse: Function to parse a string into the destination type

Returns a FlagValuer that manages the value at destination, using the type's String() method for string representation.

Example:

var duration time.Duration
valuer := NewStringerFlagValuer(
    &duration,
    func(s string) (time.Duration, error) { return time.ParseDuration(s) },
)

type Hook

type Hook struct {
	// BeforeCommandExecution is called before the command's Execute method is invoked.
	BeforeCommandExecution HookFunc
	// AfterCommandExecution is called after the command's Execute
	// method has completed (regardless of whether it returned an error).
	AfterCommandExecution HookFunc
}

Hook defines callbacks that are executed during the command lifecycle.

type HookFunc

type HookFunc func(ctx context.Context) error

HookFunc defines the signature for hook functions.

type PersistentHook

type PersistentHook struct {
	// BeforeFlagsDefinition is called before the command's flags are
	// processed. This is a good place to set up dependencies or
	// perform initialization that affects flag parsing.
	BeforeFlagsDefinition HookFunc
	// BeforeCommandExecution is called before the command's Execute
	// method is invoked (but after flag parsing).
	BeforeCommandExecution HookFunc
	// AfterCommandExecution is called after the command's Execute
	// method has completed (regardless of whether it returned an error).
	AfterCommandExecution HookFunc
}

PersistentHook defines callbacks that are executed for a command and all of its subcommands.

type ShowHelpError

type ShowHelpError interface {
	error
	ShowHelp() bool
}

ShowHelpError is an interface that allows commands to signal whether the help message should be displayed along with the error.

Directories

Path Synopsis
cfg
Package clicfg provides a flexible configuration system for CLI applications.
Package clicfg provides a flexible configuration system for CLI applications.
internal

Jump to

Keyboard shortcuts

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