cli

package module
v0.0.0-...-de7760a Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: Apache-2.0 Imports: 27 Imported by: 114

README

StreamingFast CLI Library

reference License

Quick and opinionated library aiming to create CLI application rapidly. The library contains CLI primitives (around Cobra/Viper) as well as low-level primitives to ease the creation of developer scripts.

Note This library is experimental and the API could change without notice.

Example

The folder ./example contains example usage of the library. You can run them easily them, open a terminal and navigate in the example folder, then go run ...

  • Flat - go run ./flat
  • Nested - go run ./nested
Sample Boilerplate (copy/paste ready)
package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	. "github.com/streamingfast/cli"
	"github.com/streamingfast/logging"
)

// Injected at build time
var version = ""

var zlog, tracer = logging.RootLogger("project", "github.com/acme/project")

func main() {
	logging.InstantiateLoggers()

	Run(
		"runner",
		"Some random command runner with 2 sub-commands",

		ConfigureVersion(version),
		ConfigureViper("PROJECT"),

		Group(
			"generate",
			"Quick group summary, without a description",
			Command(generateImgE,
				"image",
				"Quick command summary, without a description",
				Flags(func(flags *pflag.FlagSet) {
					flags.String("version", "", "A flag description")
				}),
			),
		),

		Command(compareE,
			"compare <input_file>",
			"Quick command summary, with a description, the actual usage above is descriptive, you must handle the arguments manually",
			Description(`
				Description of the command, automatically de-indented by using first line indentation,
				use 'go run ./example/nested compare --help' to see it in action!
			`),
			ExamplePrefixed("runner", `
				compare relative_file.json
				compare /absolute/file.json
			`),
			ExactArgs(2),
		),

		OnCommandErrorLogAndExit(zlog),
	)
}

func generateImgE(cmd *cobra.Command, args []string) error {
	fmt.Println("Generating something from", cli.WorkingDirectory())
	return errors.New("showing on error look like (if you used `go run`, the 'exit status 1' is printed by it, compiled binary will not print it)")
}

func compareE(cmd *cobra.Command, args []string) error {
	shouldContinue, wasAnswered := cli.PromptConfirm(`Do you want to continue?`)
	if wasAnswered && shouldContinue {
		fmt.Println("Showing diff between files")
	} else {
		fmt.Println("Not showing diff between files, run the following command to see it manually:")
	}

	return nil
}
Comparison between cli.Command and &cobra.Command manual construction

The best way to see the difference is by opening the before/after in your browser:

And enjoy the feeling.

Note The cli library is a wrapper around cobra.Command, so at the end you still deal with *cobra.Command.

Contributing

Issues and PR in this repo related strictly to the cli library.

Report any protocol-specific issues in their respective repositories

Please first refer to the general StreamingFast contribution guide, if you wish to contribute to this codebase.

This codebase uses unit tests extensively, please write and run tests.

License

Apache 2.0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	PromptTypeOptionalString = func(x string) (*string, error) {
		if x == "" {
			return nil, nil
		}
		return &x, nil
	}
	PromptTypeString = func(x string) (string, error) { return x, nil }
	PromptTypeInt    = strconv.Atoi
	PromptTypeInt64  = func(x string) (int64, error) { return strconv.ParseInt(x, 0, 64) }
	PromptTypeUint64 = func(x string) (uint64, error) { return strconv.ParseUint(x, 0, 64) }
	PromptTypeYesNo  = func(in string) (bool, error) { in = strings.ToLower(in); return in == "y" || in == "yes", nil }
)

Various prompt transformer declared like you are using standard type and generics make the rest

To be used like:

PromptT("Input your age", opts, PromptTypeUint64)
View Source
var (
	PrompValidateString = func(x string) error { return nil }
	PrompValidateInt    = func(x string) error { _, err := strconv.Atoi(x); return err }
	PrompValidateInt64  = func(x string) error { _, err := strconv.ParseInt(x, 0, 64); return err }
	PrompValidateUint64 = func(x string) error { _, err := strconv.ParseUint(x, 0, 64); return err }

	PrompValidateYesNo = func(in string) error {
		if !confirmPromptRegex.MatchString(in) {
			return errors.New("answer with y/yes/Yes or n/no/No")
		}

		return nil
	}
)

Various prompt validation declared like you are using standard validation.

To be used like:

cli.PromptT("Input your age", cli.PromptTypeUint64, cli.WithPromptValidate("invalid age", cli.PrompValidateUint64))
View Source
var ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
View Source
var HeaderStyle = lipgloss.NewStyle().Bold(true)
View Source
var OnAssertionFailure func(message string)

OnAssertionFailure is a global variable that can be overriden to control how the program should print/process when cli module assertion fail.

The message can be "" in which case it should not be printed/logged.

If your handler does not exit by itself, a call to `cli.Exit(1)` is performed after the handler has executed.

If you exit yourself, you should use `cli.Exit(code)` so that exit handlers are called if any present.

View Source
var PurpleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
View Source
var QuestionTextStyle = HeaderStyle
View Source
var ReboundFlagAnnotation = "github.com/streamingfast/cli#rebound-key"
View Source
var WarningStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("11"))

Functions

func AbsolutePath

func AbsolutePath(in string) string

func AskConfirmation

func AskConfirmation(label string, args ...any) (answeredYes bool, wasAnswered bool)

AskConfirmation prompts the user for a yes/no confirmation.

If 'label' is a multiple line string, it will be split into separate lines and each line will be rendered with the appropriate style but once answered, only the final line changes, the previous written lines are not affected.

This is why that rendering wise, favoring single line for 'label' will offer a better user experience.

func ConfigureViperForCommand

func ConfigureViperForCommand(root *cobra.Command, envPrefix string)

ConfigureViperForCommand sets env prefix to 'prefix', automatic env to check in env for any flags coming from anywhere (flag, config, default, etc.) as well as scoping flags to the command it's defined in for global acces.

func CopyFile

func CopyFile(inPath, outPath string)

func Dedent

func Dedent(input string) string

func Description

func Description(value string) description

func DirectoryExists

func DirectoryExists(path string) bool

func Ensure

func Ensure(condition bool, message string, args ...any)

Ensure checks the condition and if it is false, it will call `cli.Quit` with the message effectively exiting the program with code 1.

func Example

func Example(value string) example

func ExamplePrefixed

func ExamplePrefixed(prefix string, examples string) example

func Execute

func Execute(f func(cmd *cobra.Command, args []string) error) execute

func Exit

func Exit(code int)

Exit executes the registered exit handlers and then call `os.Exit(code)`. This can be used to implement a trap behavior. Of course, you must ensure that `os.Exit` are all done through `cli.Exit()` otherwise we will not be invoked.

**Caveats** Panics are not recovered by those exit handlers, right now you need to implement your own trapping and then exit through `cli.Exit`.

This library use `cli.Exit(code)` throughout so quitting due to `cli.NoError` or `cli.Ensure` will correctly call the exit handlers.

func ExitHandler

func ExitHandler(id string, onExit func(code int))

ExitHandler registers or unregisters an exit handler. If the `onExit` received is nil, unregister the handler with given `id`. Otherwise, register or update an existing one.

No collision is checked, so an id overrides any previously existing id. This library is meant to be used on final CLI product, you should have low number of exit handler, pick your id and keep them short.

func FileExists

func FileExists(path string) bool

func FlagDescription

func FlagDescription(in string, args ...interface{}) string

FlagDescription accepts a multi line indented description and transform it into a multi line flag description.

func FlagDescriptionOneLine

func FlagDescriptionOneLine(in string, args ...interface{}) string

FlagDescriptionOneLine accepts a multi line indented description and transform it into a single line flag description. This method is used to make it easier to define long flag messages.

func GetDisplayVersion

func GetDisplayVersion(version string) string

GetDisplayVersion is a helper function that can be used to get the same version string as the one configured by ConfigureVersion in other places of your application, in logging for example.

This string is the same that a user will see when doing `<app> --version` on the CLI.

It panics if it cannot retrieve the build info debug.ReadBuildInfo, which should not happen in normal conditions as the build info is always available in Go binaries built with module support, which is the default since Go 1.18.

func MaybePrompt

func MaybePrompt[T any](label string, transformer PromptTransformer[T], opts ...PromptOption) (T, error)

MaybePrompt is just like Prompt but may return an error

func MaybePromptConfirm

func MaybePromptConfirm(label string, opts ...PromptOption) (answer bool, wasAnswered bool, err error)

MaybePromptConfirm is just like PromptConfirm but returns an error instead of panicking.

func MaybePromptSelect

func MaybePromptSelect[T any](label string, items []string, transformer PromptTransformer[T], opts ...PromptSelectOption) (T, error)

func NoError

func NoError(err error, message string, args ...any)

NoError checks if the error is nil, and if it is not, it will call `cli.Quit` with the message effectively exiting the program with code 1.

func Prompt

func Prompt[T any](label string, transformer PromptTransformer[T], opts ...PromptOption) T

Prompt will ask the following "label" question to the user and will transform the received `string` into the generic type T via the `transformer` function. There is a few predefined transformer for common types like `PromptTypeString`, `PromptTypeInt`, `PromptTypeInt64`, `PromptTypeUint64`, `PromptTypeYesNo`. Using the right transformer will automatically infers the right T genric type.

userID := cli.Prompt("Please enter the user ID to issue the token to", cli.PromptTypeString)

In this case, the `userID` variable will be of type `string`.

If 'label' is a multiple line string, it will be split into separate lines and each line will be rendered with the appropriate style but once answered, only the final line changes, the previous written lines are not affected.

This is why that rendering wise, favoring single line for 'label' will offer a better user experience.

func PromptConfirm

func PromptConfirm(label string, opts ...PromptOption) (answer bool, wasAnswered bool)

PromptConfirm is just like Prompt but enforce `IsConfirm` and returns a boolean which is either `true` for yes answer or `false` for a no answer.

func PromptRaw

func PromptRaw(label string, opts ...PromptOption) (answer string, err error)

func PromptSelect

func PromptSelect[T any](label string, items []string, transformer PromptTransformer[T], opts ...PromptSelectOption) T

func Quit

func Quit(message string, args ...any)

Quit prints the message and exits the program with code 1. If OnAssertionFailure is set, it will be called with the message instead of printing it to stdout.

If you want to exit with a different code, use `cli.Exit(code)` instead.

func ReadFile

func ReadFile(name string) string

func Root

func Root(usage, short string, opts ...CommandOption) *cobra.Command

func Run

func Run(usage, short string, opts ...CommandOption)

func SetLogger

func SetLogger(newZlog *zap.Logger, newTracer logging.Tracer)

func SetupSignalHandler

func SetupSignalHandler(unreadyPeriodDelay time.Duration, logger *zap.Logger) (receiveOutgoingSignals <-chan os.Signal, hasBeenSignaled, waitedFullDelay *atomic.Bool)

SetupSignalHandler registers a signal handler for SIGINT and SIGTERM and returns a channel to receive determines if the service has been signaled to shutdown gracefully as well as 2 atomic booleans.

The first one `hasBeenSignaled` determines if the service has been signaled already, this could be true before the channel is notified if `unreadyPeriodDelay > 0`. The usefulness of this is to change the readiness of your app based on the value in `hasBeenSignaled`. This way, your app is unready as soon as it has been signaled to shutdown which helps a lot in Kubernetes and other orchestration systems. See the `unreadyPeriodDelay` parameter details below for more information.

The second one `waitedFullDelay` determines if the service has waited the full delay period before notifying the channel. This will be `true` unless `unreadyPeriodDelay > 0` and that `Ctrl-C` was pressed 4 times or more which forces a kill `os.Exit(1)`!

The parameter `unreadyPeriodDelay` influences when you will be notified of the signal through the channel. If you set it to 0, you will be notified immediately. If you set it to 5 seconds, you will be notified of the signal after 5 seconds.

The usefulness of this is to give your app some time to become unready before it is killed. This is important for some orchestration systems like Kubernetes where you can use this delay to mark your app as unready by checking the value of `hasBeenSignaled` while your app continues to work properly. This will give Kubernetes `unreadyPeriodDelay` time to stop sending traffic and removing from the active list of endpoints.

This should usually be used for HTTP/gRPC servers, for other types of apps, you can set this to 0 to avoid this needlessly waiting period.

func ToCobraCmd

func ToCobraCmd(cliCmd CommandOption) *cobra.Command

ToCobraCmd converts a CommandOption to a cobra.Command suitable to be added to another "parent" command using the `AddCommand` method.

func UserHomeDirectory

func UserHomeDirectory() string

func WorkingDirectory

func WorkingDirectory() string

func WriteFile

func WriteFile(name string, content string, args ...any)

WriteFile is a quick version `os.WriteFile` where NoError is used to ensure no error occur.

Types

type AfterAllHook

type AfterAllHook func(cmd *cobra.Command)

func (AfterAllHook) Apply

func (f AfterAllHook) Apply(cmd *cobra.Command)

type Application

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

Application is a simple object that can be used to manage the lifecycle of an application. You create the application with `NewApplication` and then you can use it to supervise and start child processes by using `SuperviseAndStart`.

You then wait for the application to terminate by calling `WaitForTermination`. This call is blocking and register a signal handler to be notified of SIGINT and SIGTERM.

func NewApplication

func NewApplication(ctx context.Context) *Application

func (*Application) BlockUntilTerminated

func (a *Application) BlockUntilTerminated(logger *zap.Logger, gracefulShutdownDelay time.Duration) error

BlockUntilTerminated waits for the application to terminate. This is a blocking call that will wait for the application to be terminated. After receiving the initial terminating signal, if gracefulShutdownDelay is not 0, the terminated signal must happen within the gracefulShutdownDelay otherwise the call will unblock.

func (*Application) Context

func (a *Application) Context() context.Context

func (*Application) IsReady

func (a *Application) IsReady() bool

IsReady returns true if the application is ready to be used. When the Ctrl-C signal is received, the app is immediately marked as not ready and the `WaitForTermination` call will wait for the unreadyPeriodDelay to expire before terminating the application.

func (*Application) Supervise

func (a *Application) Supervise(child Shutter)

Supervise the received child shutter, mainly, this ensures that on child's termination, the application is also terminated with the error that caused the child to terminate.

If the application shuts down before the child, the child is also terminated but gracefully (it does **not** receive the error that caused the application to terminate).

The child termination is always performed before the application fully complete, unless the gracecul shutdown delay has expired.

func (*Application) SuperviseAndStart

func (a *Application) SuperviseAndStart(child Shutter)

SuperviseAndStart calls [Supervise] and then starts the child in a goroutine. The received child must implement one of Runnable, RunnableContext, RunnableError or RunnableContextError to be able to be started correctly.

The child is started in a goroutine and tied to the application lifecycle because we also called [Supervise]. Later the call to `WaitForTermination` will wait for the application to terminate which will also terminates and wait for all child.

People can use either [SuperviseAndStart] or [SuperviseAndStartUsing] and passing `child.Run` explicitly depending on their preference, see [SuperviseAndStartUsing] for details.

func (*Application) SuperviseAndStartUsing

func (a *Application) SuperviseAndStartUsing(child Shutter, runner any)

SuperviseAndStartUsing calls [Supervise] and then starts the child in a goroutine using the provided runner function. The runner function must be one of the following types:

func()
func(ctx context.Context)
func() error
func(ctx context.Context) error

This can be used to make it more explicit what is the runner function that is started, making the `Run` method apparent at call site. Compare:

a.SuperviseAndStart(child)

with

a.SuperviseAndStartUsing(child, child.Run)

In the second case, you can "<key>-click" to go to the `Run` method of the child directly, while in the first case, you have to search, the linking is not as direct.

People can use either [SuperviseAndStart] or [SuperviseAndStartUsing] depending on their preference.

func (*Application) WaitForTermination

func (a *Application) WaitForTermination(logger *zap.Logger, unreadyPeriodDelay, gracefulShutdownDelay time.Duration) error

WaitForTermination waits for the application to terminate. This first setup the signal handler and then wait for either the signal handler to be notified or the application to be terminating.

On application terminating, all child registered with [Supervise] are also terminated. We then wait for all child to gracefully terminate. If the graceful shutdown delay is reached, we force the termination of the application right now.

Doing Ctrl-C 4 times or more will lead to a force quit of the whole process by calling `os.Exit(1)`, this is performed by the signal handler code and is does **not** respect the graceful shutdown delay in this case.

type Args

type Args cobra.PositionalArgs

func ArbitraryArgs

func ArbitraryArgs() Args

ArbitraryArgs never returns an error.

func ExactArgs

func ExactArgs(n int) Args

ExactArgs returns an error if there are not exactly n args.

func ExactValidArgs

func ExactValidArgs(n int) Args

ExactValidArgs returns an error if there are not exactly N positional args OR there are any positional args that are not in the `ValidArgs` field of `Command`

func MaximumNArgs

func MaximumNArgs(n int) Args

MaximumNArgs returns an error if there are more than N args.

func MinimumNArgs

func MinimumNArgs(n int) Args

MinimumNArgs returns an error if there is not at least N args.

func NoArgs

func NoArgs() Args

NoArgs returns an error if any args are included.

func OnlyValidArgs

func OnlyValidArgs() Args

OnlyValidArgs returns an error if any args are not in the list of ValidArgs.

func RangeArgs

func RangeArgs(min int, max int) Args

RangeArgs returns an error if the number of args is not within the expected range.

func (Args) Apply

func (a Args) Apply(cmd *cobra.Command)

type BeforeAllHook

type BeforeAllHook func(cmd *cobra.Command)

func (BeforeAllHook) Apply

func (f BeforeAllHook) Apply(cmd *cobra.Command)

type CommandOption

type CommandOption interface {
	Apply(cmd *cobra.Command)
}

func Command

func Command(execute func(cmd *cobra.Command, args []string) error, usage, short string, opts ...CommandOption) CommandOption

func ConfigureVersion

func ConfigureVersion(version string) CommandOption

ConfigureVersion is an option that configures the `cobra.Command#Version` field automatically based fetching commit revision and date build from Golang available built-in build info.

The version formatted output can take different forms depending on the state of 'vcs.revision' availability, 'vcs.time' availability and received 'version':

if vcs.revision == "" && vcs.time == "" return "{version}"
if vcs.revision != "" && vcs.time == "" return "{version} (Commit {vcs.revision[0:7]})"
if vcs.revision == "" && vcs.time == "" return "{version} (Commit Date {vcs.date})"
if vcs.revision != "" && vcs.time != "" return "{version} (Commit {vcs.revision[0:7]}, Commit Date {vcs.date})"

func ConfigureViper

func ConfigureViper(envPrefix string) CommandOption

ConfigureViper installs an AfterAllHook on the cobra.Command that rebind all your flags into viper with a new layout.

Persistent flags on a root command can be accessed with `global-<flag>` Persistent flags on a sub-command can be accessed with `<cmd1>-<cmd2>-global-<flag>` where `<cmd1>-<cmd2>` is the command fully qualified path (see below for more details). Standard flags on a command can be accessed with `<cmd1>-<cmd2>-<flag>` where `<cmd1>-<cmd2>` is the command fully qualified path (see below for more details).

For the following config:

Root("acme", "CLI sample application",
	PersistentFlags(func(flags *pflag.FlagSet) { flags.String("auth", "", "Auth token") }),

	Group("tools", "Tools for developers",
		PersistentFlags(func(flags *pflag.FlagSet) { flags.Bool("dev", false, "Dev mode") }),

		Command(toolsReadE, "read",
			"Read command",
			Flags(func(flags *pflag.FlagSet) { flags.Bool("skip-errors", false, "Skip read errors") }),
		),

		Command(toolsWriteE, "write",
			"Write command",
			Flags(func(flags *pflag.FlagSet) { flags.Bool("skip-errors", false, "Skip write errors") })),
	),
)

Which renders the follow CLI hierarchy of commands:

acme (--auth <auth>)
 tools (--dev)
   read (--skip-errors)
   write (--skip-errors)

You can access the flags using viper sub-commands through this hierarchy:

viper.GetString("global-auth")             // acme --auth ""
viper.GetBool("tools-global-dev")          // acme tools --dev
viper.GetString("tools-read-skip-errors")  // acme tools read --skip-errors
viper.GetString("tools-write-skip-errors") // acme tools write --skip-errors

And also with environment variables:

viper.GetString("global-auth")             // {PREFIX}_GLOBAL_AUTH=<auth> acme
viper.GetBool("tools-global-dev")          // {PREFIX}_TOOLS_GLOBAL_DEV=<dev> acme acme tools
viper.GetString("tools-read-skip-errors")  // {PREFIX}_TOOLS_READ_SKIP_ERRORS=<skip> acme tools read
viper.GetString("tools-write-skip-errors") // {PREFIX}_TOOLS_WRITE_SKIP_ERRORS=<skip> acme tools write

Priority is: Flag definition via CLI overrides everyone else (--<flag>) Environment overrides values provided by config file or defaults ({PREFIX}_{ENV_KEY}) Config file (if configure separately) overrides defaults values (if configure separately) (--<flag>) Defaults values defined on the flag definition directly

func Group

func Group(usage, short string, opts ...CommandOption) CommandOption

func OnCommandError

func OnCommandError(onError func(err error)) CommandOption

OnCommandError intercepts error returned when running your `cobra.Command.RunE` handler and enable you to do something with it, like logging it. Once your handler has finish running, the process will exit with code 1.

You are free to exit yourself in your own handler, for example if some error should still exit with code 0.

func OnCommandErrorLogAndExit

func OnCommandErrorLogAndExit(logger *zap.Logger) CommandOption

OnCommandErrorLogAndExit logs the error to the logger, sync the logger and exit with 1. It also intercepts assertion error produced by this library through `cli.Ensure` and `cli.NoError`.

type CommandOptionFunc

type CommandOptionFunc func(cmd *cobra.Command)

func (CommandOptionFunc) Apply

func (f CommandOptionFunc) Apply(cmd *cobra.Command)

type Flags

type Flags func(flags *pflag.FlagSet)

func (Flags) Apply

func (f Flags) Apply(cmd *cobra.Command)

type OnCommandErrorHandler

type OnCommandErrorHandler func(err error)

type PersistentFlags

type PersistentFlags func(flags *pflag.FlagSet)

func (PersistentFlags) Apply

func (f PersistentFlags) Apply(cmd *cobra.Command)

type PromptOption

type PromptOption interface {
	Apply(opts *promptOptions)
}

func WithPromptConfirm

func WithPromptConfirm() PromptOption

func WithPromptDefaultValue

func WithPromptDefaultValue(in string) PromptOption

func WithPromptTemplates

func WithPromptTemplates(templates *promptui.PromptTemplates) PromptOption

func WithPromptValidate

func WithPromptValidate(label string, fn promptui.ValidateFunc) PromptOption

type PromptSelectOption

type PromptSelectOption interface {
	Apply(opts *promptSelectOptions)
}

func WithPromptSelectTemplates

func WithPromptSelectTemplates(templates *promptui.SelectTemplates) PromptSelectOption

type PromptTransformer

type PromptTransformer[T any] func(string) (T, error)

type Runnable

type Runnable interface {
	Run()
}

Runnable contracts is to be blocking and to return only when the task is done.

type RunnableContext

type RunnableContext interface {
	Run(ctx context.Context)
}

RunnableContext contracts is to be blocking and to return only when the task is done. The context must be **used** only for the bootstrap period of task, long running task should be tied to a `*shutter.Shutter` instance.

type RunnableContextError

type RunnableContextError interface {
	Run(ctx context.Context) error
}

RunnableContext contracts is to be blocking and to return only when the task is done. The context must be **used** only for the bootstrap period of task, long running task should be tied to a `*shutter.Shutter` instance. We assume the error happens while bootstrapping the task.

type RunnableError

type RunnableError interface {
	Run() error
}

RunnableError contracts is to be blocking and to return only when the task is done. We assume the error happens while bootstrapping the task.

type Shutter

type Shutter interface {
	OnTerminated(f func(error))
	OnTerminating(f func(error))
	Shutdown(error)
}

Shutter interface over the `*shutter.Shutter` struct.

Directories

Path Synopsis
example module
Code generated by generate_flags, DO NOT EDIT!
Code generated by generate_flags, DO NOT EDIT!
generator command

Jump to

Keyboard shortcuts

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