command

package module
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: Jul 31, 2024 License: MIT Imports: 18 Imported by: 13

README

command

Maintainer GoVersion GoDoc GoReportCard

CLI Command-based framework with extra sugar!

This small framework is intended to help in creating simple CLI applications with a hierarchical command-based, with built-in configuration conforming to the 12-factor app manifest, yet without requiring the developer to write a lot of boilerplate code.

It has the following goals:

  • Utilize & play nice with the builtin Go flag package
  • Builtin usage & help screens
  • Hierarchical command structure
  • Easy & simple (though opinionated) configuration facility

Attribution

This library is heavily inspired by Cobra, an excellent command framework. I do feel, however, that it is a bit too heavy weight for just allowing users to write main CLI programs yet with the familiar (kubectl-like) command hierarchy and easy configuration. Cobra, unfortunately, carries much more water - for good and worse. It is packed with features and facilities which I believe most programs do not really need.

That said, big respect goes to spf13 for creating that library - it is really exceptional, and has garnered a huge community behind it. If you're looking for an all-in-one solution for extensive configuration, hooks, code generators, etc - Cobra is my recommendation.

Usage

Create the following file structure:

demo
 |
 +-- main.go
 |
 +-- root.go
 |
 +-- command1.go
 |
 +-- command2.go
 |
 ...

For command2.go use this:

package main

import (
	"context"
	"fmt"

	"github.com/arikkfir/command"
)

type Command2 struct {
	MyFlag1 string `flag:"true"`
	MyFlag2 int    `desc:"mf2" required:"true"`
}

func (c *Command2) PreRun(ctx context.Context) error {
	// Invoked every time this command or any of its sub-commands are run
	fmt.Println(c.MyFlag1)
	return nil
}

func (c *Command2) Run(ctx context.Context) error {
	// Invoked when this command is run
	fmt.Println(c.MyFlag2)
	return nil
}

var cmd2 = command.MustNew(
	"command2",
	"This is command2, a magnificent command that does something.",
	`Longer description...`,
	&Command2{
		MyFlag1: "default value for --my-flag1",
	},
)

For command1.go use this:

package main

import (
	"context"
	"fmt"

	"github.com/arikkfir/command"
)

type Command1 struct {
	AnotherFlag bool   `flag:"true"`
	MyURL       string `value-name:"URL" env:"HTTP_URL"`
}

func (c *Command1) PreRun(ctx context.Context) error {
	// Invoked every time this command or any of its sub-commands are run
	fmt.Println(c.AnotherFlag)
	return nil
}

func (c *Command1) Run(ctx context.Context) error {
	// Invoked when this command is run
	fmt.Println(c.MyURL)
	return nil
}

var cmd1 = command.MustNew(
	"command1",
	"This is command1, a magnificent command that does something.",
	`Longer description...`,
	&Command1{
		MyURL: "default value for --my-url",
	},
	cmd2, // Adding cmd2 as a sub-command of cmd1
)

For root.go, use this:

package main

import (
	"context"
	"fmt"

	"github.com/arikkfir/command"
)

type Root struct {
	Args []string `args:"true"`
	Port int      `value-name:"PORT" env:"HTTP_PORT"`
}

func (c *Root) PreRun(ctx context.Context) error {
	// Invoked every time this command or any of its sub-commands are run
	fmt.Println(c.Port)
	return nil
}

func (c *Root) Run(ctx context.Context) error {
	// Invoked when this command is run
	fmt.Println(c.Port)
	return nil
}

var root = command.MustNew(
	filepath.Base(os.Args[0]),
	"This is the root command.",
	`This is the command executed when no sub-commands are specified in the command line, e.g. like
running "kubectl" and pressing ENTER.`,
	&Root{
		Port: "default value for --port",
	},
	cmd1, // Adding cmd1 as a sub-command of root
)

And finally create main.go like so:

package main

import (
	"context"
	"os"

	"github.com/arikkfir/command"
)

func main() {
	command.Execute(context.Context(), os.Stderr, root, os.Args, command.EnvVarsArrayToMap(os.Environ()))
}

Running

Once your program is compiled to a binary, you can run it like so:

$ myprogram --some-flag=someValue # this will run the root command; since no "Run" function was provided, it will print the usage screen
$ myprogram --some-flag=someValue command1 # runs "command1" with the flag from root, and the default value for "AnotherFlag"
$ myprogram --some-flag=someValue command1 --another-flag=anotherValue # runs "command1" with the flag from root, and the value for "AnotherFlag"
$ myprogram command1 --another-flag=anotherValue # runs "command1" with the default value for the root flag, and the value for "AnotherFlag"
$ myprogram command1 command2 # runs the "command2" command

Usage & Help screens

For the root command (just running myprogram), this would be the usage page:

$ myprogram --help
myprogram: This is the root command.

This is the command executed when no sub-commands are specified in the command line, e.g. like
running "kubectl" and pressing ENTER.

Usage:
	myprogram [--some-flag] [--help]

Flags:
	--some-flag    This flag is a demo flag of type string.
	--help         Print usage information (default is false)

Sub-commands:
	command1       This is command1, a magnificent command that does something.

For command1, this would be the usage page:

$ myprogram command1 --help
myprogram command1: This is command1, a magnificent command that does something.

Longer description...

Usage:
	myprogram command1 [--some-flag] --another-flag [--help]

Flags:
	--some-flag    This flag is a demo flag of type string.
	--another-flag This is another flag, of type int.
	--help         Print usage information (default is false)

Sub-commands:
    command2       This is command2, another magnificent command that does something.

For command2, notice that since it doesn't have a long description set, nor any sub-commands, none are printed:

$ myprogram command1 command2 --help
myprogram command1 command2: This is command2, another magnificent command that does something.

Usage:
	myprogram command1 [--some-flag] --another-flag [--yet-another-flag] [--help] [ARGS]

Flags:
	--some-flag         This flag is a demo flag of type string.
	--another-flag      This is another flag, of type int.
	--yet-another-flag  And another one...
	--help              Print usage information (default is false)

Naming of flags & environment variables

Fields in command configuration structs should be named in standard Go pascal-case (MyField).

Flags for fields will be generated, with flag names as kebab-case (--my-field).

Environment variables will be generated as an upper-case snake-case (MY_FIELD).

Field tags

You can use Go tags for the configuration fields:

package main

type MyCommand struct {
	FlagWithDefaults  string   `flag:"true"`
	ModifyCLIFlagName string   `name:"another-name"`     // Use "another-name" instead of "modify-cli-flag-name"
	ModifyEnvVarName  string   `env:"CUSTOM"`            // Use "CUSTOM" env-var instead of "MODIFY_CLI_ENV_VAR_NAME"
	ModifyValueName   string   `value-name:"PORT"`       // Show "--modify-value-name=PORT" instead of "--modify-value-name=VALUE" on help screen
	ModifyDesc        string   `desc:"Flag description"` // Describe what this flag does
	ModifyRequired    string   `required:"true"`         // Make the flag required
	ModifyInherited   string   `inherited:"true"`        // Sub-commands will get this flag as well
	Args              []string `args:"true"`             // This field will get all the non-flag positional arguments for the command
}

Field types

Configuration fields cab be of type string, int, uint, float64, bool, or a struct containing additional flags. New types will be added soon (e.g. time.Time, time.Duration, net.IP, and more).

Contributing

Please do 👌 💪 !

See CONTRIBUTING.md for more information 🙏

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidCommand          = errors.New("invalid command")
	ErrCommandAlreadyHasParent = errors.New("command already has a parent")
)

Functions

func EnvVarsArrayToMap

func EnvVarsArrayToMap(envVars []string) map[string]string

func SetupSignalHandler added in v0.0.2

func SetupSignalHandler() context.Context

SetupSignalHandler registers for SIGTERM and SIGINT. A context is returned which is canceled on one of these signals. If a second signal is caught, the program is terminated with exit code 1.

Types

type Action added in v0.4.0

type Action interface {
	Run(context.Context) error
}

type ActionFunc added in v0.4.0

type ActionFunc func(context.Context) error

func (ActionFunc) Run added in v0.4.0

func (i ActionFunc) Run(ctx context.Context) error

type Command

type Command struct {
	HelpConfig *HelpConfig
	// contains filtered or unexported fields
}

Command is a command instance, created by New and can be composed with more Command instances to form a CLI command hierarchy.

func MustNew added in v0.3.0

func MustNew(name, shortDescription, longDescription string, action Action, hooks []any, subCommands ...*Command) *Command

MustNew creates a new command using New, but will panic if it returns an error.

func New

func New(name, shortDescription, longDescription string, action Action, hooks []any, subCommands ...*Command) (*Command, error)

New creates a new command with the given name, short & long descriptions, and the given executor. The executor object is also scanned for configuration structs via reflection.

func (*Command) AddSubCommand added in v0.3.0

func (c *Command) AddSubCommand(cmd *Command) error

AddSubCommand will add the given command as a sub-command of this command. An error is returned if the given command already has another parent.

func (*Command) PrintHelp added in v0.3.0

func (c *Command) PrintHelp(w io.Writer, width int) error

func (*Command) PrintUsageLine added in v0.3.0

func (c *Command) PrintUsageLine(w io.Writer, width int) error

type ErrInvalidTag added in v0.3.0

type ErrInvalidTag struct {
	Cause error
	Tag   Tag
	Value string
}

func (*ErrInvalidTag) Error added in v0.3.0

func (e *ErrInvalidTag) Error() string

func (*ErrInvalidTag) Unwrap added in v0.3.0

func (e *ErrInvalidTag) Unwrap() error

type ErrInvalidValue added in v0.3.0

type ErrInvalidValue struct {
	Cause error
	Value string
	Flag  string
}

func (*ErrInvalidValue) Error added in v0.3.0

func (e *ErrInvalidValue) Error() string

func (*ErrInvalidValue) Unwrap added in v0.3.0

func (e *ErrInvalidValue) Unwrap() error

type ErrRequiredFlagMissing added in v0.3.0

type ErrRequiredFlagMissing struct {
	Cause error
	Flag  string
}

func (*ErrRequiredFlagMissing) Error added in v0.3.0

func (e *ErrRequiredFlagMissing) Error() string

func (*ErrRequiredFlagMissing) Unwrap added in v0.3.0

func (e *ErrRequiredFlagMissing) Unwrap() error

type ErrUnknownFlag added in v0.3.0

type ErrUnknownFlag struct {
	Cause error
	Flag  string
}

func (*ErrUnknownFlag) Error added in v0.3.0

func (e *ErrUnknownFlag) Error() string

func (*ErrUnknownFlag) Unwrap added in v0.3.0

func (e *ErrUnknownFlag) Unwrap() error

type ExitCode added in v0.3.0

type ExitCode int
const (
	ExitCodeSuccess          ExitCode = 0
	ExitCodeError            ExitCode = 1
	ExitCodeMisconfiguration ExitCode = 2
)

func Execute

func Execute(w io.Writer, root *Command, args []string, envVars map[string]string) ExitCode

Execute the correct command in the given command hierarchy (starting at "root"), configured from the given CLI args and environment variables. The command will be executed with a context that gets canceled when an OS signal for termination is received, after all pre-RunFunc hooks have been successfully executed in the command hierarchy.

func ExecuteWithContext added in v0.7.0

func ExecuteWithContext(ctx context.Context, w io.Writer, root *Command, args []string, envVars map[string]string) (exitCode ExitCode)

ExecuteWithContext the correct command in the given command hierarchy (starting at "root"), configured from the given CLI args and environment variables. The command will be executed with the given context after all pre-RunFunc hooks have been successfully executed in the command hierarchy.

type HelpConfig added in v0.3.0

type HelpConfig struct {
	Help bool `inherited:"true" desc:"Show this help screen and exit."`
}

HelpConfig is a configuration added to every executed command, for automatic help screen generation.

type PostRunHook added in v0.5.0

type PostRunHook interface {
	PostRun(context.Context, error, ExitCode) error
}

type PostRunHookFunc added in v0.5.0

type PostRunHookFunc func(context.Context, error, ExitCode) error

func (PostRunHookFunc) PostRun added in v0.5.0

func (i PostRunHookFunc) PostRun(ctx context.Context, err error, exitCode ExitCode) error

type PreRunHook added in v0.4.0

type PreRunHook interface {
	PreRun(context.Context) error
}

type PreRunHookFunc added in v0.4.0

type PreRunHookFunc func(context.Context) error

func (PreRunHookFunc) PreRun added in v0.4.0

func (i PreRunHookFunc) PreRun(ctx context.Context) error

type Tag added in v0.3.0

type Tag string
const (
	TagFlag        Tag = "flag"
	TagName        Tag = "name"
	TagEnv         Tag = "env"
	TagValueName   Tag = "value-name"
	TagDescription Tag = "desc"
	TagRequired    Tag = "required"
	TagInherited   Tag = "inherited"
	TagArgs        Tag = "args"
)

type WrappingWriter added in v0.3.0

type WrappingWriter struct {
	// contains filtered or unexported fields
}

func NewWrappingWriter added in v0.3.0

func NewWrappingWriter(width int) (*WrappingWriter, error)

func (*WrappingWriter) SetLinePrefix added in v0.3.0

func (w *WrappingWriter) SetLinePrefix(prefix string) error

func (*WrappingWriter) String added in v0.3.0

func (w *WrappingWriter) String() string

func (*WrappingWriter) Write added in v0.3.0

func (w *WrappingWriter) Write(p []byte) (n int, err error)

Jump to

Keyboard shortcuts

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