command

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: May 16, 2024 License: MIT Imports: 14 Imported by: 0

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 root.go, use this:

package main

import (
	"os"
	"path/filepath"

	"github.com/arikkfir/command"
)

type RootConfig struct{
	SomeFlag string `desc:"This flag is a demo flag of type string."` // We can provide flag descriptions
}

var rootCommand = command.New(nil, command.Spec{
	Name:             filepath.Base(os.Args[0]),
	ShortDescription: "This is the root command.",
	LongDescription: `This is the command executed when no sub-commands are specified in the command line, e.g. like
running "kubectl" and pressing ENTER.`,
	Config: &RootConfig{
		SomeFlag1: "default value for this flag, unless given in CLI via --some-flag or via environment variables as SOME_FLAG",
	},
})

For command1.go use this:

package main

import (
	"context"
	"fmt"

	"github.com/arikkfir/command"
)

type Command1Config struct {
	RootConfig // Root command's configuration can also be provided to this command (e.g. "--some-flag")
	AnotherFlag int `config:"required" desc:"This is another flag, of type int."` // Notice how we made this flag required
}

var cmd1Command = command.New(rootCommand, command.Spec{
	Name:             "command1",
	ShortDescription: "This is command1, a magnificent command that does something.",
	LongDescription:  `Longer description...`,
	Config:           &Command1Config{
		RootConfig: RootConfig{
			SomeFlag: "override the default value",
		},
		AnotherFlag: "default for AnotherFlag", // set in CLI as "--another-flag" or environment variable ANOTHER_FLAG
	},
	Run: func(ctx context.Context, anyConfig any, utils command.UsagePrinter) error {
		config := anyConfig.(*Command1Config)
		fmt.Printf("Running command1! Configuration is: %+v\n", config)
		return nil
	},
})

For command2.go use this:

package main

import (
	"context"
	"fmt"

	"github.com/arikkfir/command"
)

type Command2Config struct {
	Command1Config
	YetAnotherFlag string `desc:"And another one..."`
	Positionals []string  `config:"args"` // This field will get all the non-flag positional arguments for the command
}

var cmd2Command = command.New(cmd1Command, command.Spec{
	Name:             "command2",
	ShortDescription: "This is command2, another magnificent command that does something.",
	Config:           &Command1Config{
		Command1Config: Command1Config{
			RootConfig: RootConfig{
				SomeFlag: "override the default value",
			},
			AnotherFlag: "default value", 
		},
		YetAnotherFlag: "default for YetAnotherFlag",
	},
	Run: func(ctx context.Context, anyConfig any, utils command.UsagePrinter) error {
		config := anyConfig.(*Command2Config)
		fmt.Printf("Running command2! Configuration is: %+v\n", config)
		return nil
	},
})

And finally create main.go like so:

package main

import (
	"os"

	"github.com/arikkfir/command"
)

func main() {
	command.Execute(rootCommand, 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 MyConfig struct {
	RequiredField string `config:"required" desc:"This is a required field."`
	IgnoredField string  `config:"ignore"` // No flags will be generated for this field and it is not configurable via environment variables
	Args []string        `config:"args"` // 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. When providing configuration in the command spec, you must provide a pointer to the configuration struct.

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 Version = "0.0.0-unknown"

Functions

func EnvVarsArrayToMap

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

func Execute

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

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 BuiltinConfig

type BuiltinConfig struct {
	Help bool `desc:"Show help about how to use this command"`
}

type Command

type Command struct {
	Name             string
	ShortDescription string
	LongDescription  string
	Config           any
	PreSubCommandRun func(ctx context.Context, config any, usagePrinter UsagePrinter) error
	Run              func(ctx context.Context, config any, usagePrinter UsagePrinter) error
	// contains filtered or unexported fields
}

func New

func New(parent *Command, spec Spec) *Command

func (*Command) PrintFullUsage

func (c *Command) PrintFullUsage(w io.Writer)

func (*Command) PrintShortUsage

func (c *Command) PrintShortUsage(w io.Writer)

type Spec

type Spec struct {
	Name             string
	ShortDescription string
	LongDescription  string
	Config           any
	OnSubCommandRun  func(ctx context.Context, config any, usagePrinter UsagePrinter) error
	Run              func(ctx context.Context, config any, usagePrinter UsagePrinter) error
}

type UsagePrinter

type UsagePrinter interface {
	PrintShortUsage(w io.Writer)
	PrintFullUsage(w io.Writer)
}

Jump to

Keyboard shortcuts

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