cli

package
v0.296.0 Latest Latest
Warning

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

Go to latest
Published: Apr 9, 2025 License: Apache-2.0 Imports: 23 Imported by: 0

README

CLI

The cli package is designed to help you build command-line interface applications easily.

It follows a familiar structure, similar to the net/http stdlib package, so developers can quickly adapt using their existing knowledge.

The cli package also aim to simplify testing of CLI commands, allowing you to test cleanly without needing to create stubs, mocks, or fake shared values in your application.

Terminology similarities between HTTP and CLI:

HTTP CLI desc
request path command name in args defines what handler/command the caller wishes to reach
request path parameters command arguments in args endpoint specific parameters
request body STDIN contains the user input data payload
response body STDOUT the channel in which the application replies back to the caller
request query string flags interaction related meta data or modifiers that expect the affect to be altered
request headers env variables same
status code exit code code that notifies the caller if request succeeded or failed
request cancellation OS Signal interrupt an idiom to notify the software that the response no longer expected by the caller

Quick Start

To create a CLI command, you simply need to design a structure that implements the cli.Handler interface.

This structure can list all its options and arguments, which are automatically parsed and displayed in the command’s documentation when help is requested.

Fields in the structure represent the command’s dependencies.

You can use specific tags to define how these fields should behave:

  • flag: Marks the field as a CLI option for your command.
  • arg: Indicates that the field is expected as a positional argument at a specific index.
  • env: The environment variable key that can be used as an alternative way to a flag configure the CLI command.
    • flag and an env can coexist for a field, where flag will be prioritised over env.

Additional Tags for Further Specification

You can combine these tags to refine your command:

  • desc: Provides a description of the given flag or argument.
  • default: Sets a default value if the user does not supply one.
  • required: Marks the field as mandatory, ensuring the user provides it.
  • enum: Set a list of enumerator value for the field, which will define what values the CLI accept for a given input.
    • the -help documentation will list the acceptable values.
type TestCommand struct {
	BoolFlag   bool   `flag:"bool" env:"BOOLENVVAR" desc:"a bool flag"`
	StringFlag string `flag:"str" env:"STR_ENVVAR" default:"foo"`

	StringArg string `arg:"0" required:"true"`
	IntArg    int    `arg:"1" default:"42"`
}

func (cmd TestCommand) ServeCLI(w cli.Response, r *cli.Request) {
	fmt.Fprintln(w, pp.Format(cmd))
}
Usage: direct [OPTION]... [StringArg] [IntArg]

Options:
  -bool=[bool]: a bool flag (env: BOOLENVVAR)
  -str=[string] (env: STR_ENVVAR) (default: foo)

Arguments:
  StringArg [string]
  IntArg [int] (Default: 42)

Testing

Testing with frameless/pkg/cli is designed to be simple. Just set up your command value based on your testing scenario and call ServeCLI on it.

package main

import (
	"testing"

	"go.llib.dev/frameless/pkg/cli"
)

func TestMyCommand(t *testing.T) {
	cmd := MyCommand{ // setting up your flag/arg configuration
		BoolFlag:   true,
		StringFlag: "foo",
		StringArg:  "bar",
		IntArg:     42,
	}

	rr := &cli.ResponseRecorder{}
	req := &cli.Request{}

	cmd.ServeCLI(rr, req)
}

Dependency injection

If your command/cli.Handler has its own dependencies, you can simply pass the preconfigured command structure as the handler, the dependencies won't be affected by argument parsing.

func main() {
	cli.Main(context.Background(), CommandWithDependency{
		Dependency: "important dependency that I need as part of the ServeCLI call",
	})
}

Example

direct
package main

import (
	"context"
	"fmt"

	"go.llib.dev/frameless/pkg/cli"
	"go.llib.dev/testcase/pp"
)

func main() {
	cli.Main(context.Background(), TestCommand{})
}

type TestCommand struct {
	BoolFlag   bool   `flag:"bool" desc:"a bool flag"`
	StringFlag string `flag:"str" default:"foo"`

	StringArg string `arg:"0" required:"true"`
	IntArg    int    `arg:"1" default:"42"`
}

func (cmd TestCommand) ServeCLI(w cli.Response, r *cli.Request) {
	fmt.Fprintln(w, pp.Format(cmd))
}
multi command setup

The cli.Mux allows you to dispatch the cli request between commands and sub-commands in your application.

package main

import (
	"context"
	"fmt"

	"go.llib.dev/frameless/pkg/cli"
	"go.llib.dev/testcase/pp"
)

func main() {
	var m cli.Mux
	m.Handle("test", TestCommand{})
	m.Handle("foo", FooCommand{})
	m.Handle("baz", BazCommand{})

	sub := m.Sub("sub")
	sub.Handle("bar", BarCommand{})
	// works also with m.Handle("sub bar", BarCommand{})

	cli.Main(context.Background(), m)
}

type TestCommand struct {
	BoolFlag   bool   `flag:"bool"`
	StringFlag string `flag:"str" default:"foo"`

	StringArg string `arg:"0" default:"foo"`
	IntArg    int    `arg:"1" default:"42"`
}

func (cmd TestCommand) ServeCLI(w cli.Response, r *cli.Request) {
	fmt.Fprintln(w, pp.Format(cmd))
}

type FooCommand struct {
	A string `flag:"the-a,a" default:"val"   desc:"this is flag A"`
	B bool   `flag:"the-b,b" default:"true"` // missing description
	C int    `flag:"c" required:"true"       desc:"this is flag C, not B"`
	D string `flag:"d" enum:"FOO,BAR,BAZ,"   desc:"this flag is an enum"`

	Arg    string `arg:"0" desc:"something something"`
	OthArg int    `arg:"1" default:"42"`

	// Dependency is a dependency of the FooCommand, which is populated though traditional dependency injection.
	Dependency string
}

func (cmd FooCommand) Summary() string { return "foo command" }

func (cmd FooCommand) ServeCLI(w cli.Response, r *cli.Request) {
	fmt.Fprintf(w, "%#v\n", cmd)
}

type BarCommand struct{}

func (cmd BarCommand) ServeCLI(w cli.Response, r *cli.Request) {
	fmt.Fprintln(w, "bar")
}

type BazCommand struct {
	First  string `arg:"0"`
	Second string `arg:"1"`
}

func (cmd BazCommand) ServeCLI(w cli.Response, r *cli.Request) {
	fmt.Fprintln(w, "baz")
}

Documentation

Overview

Example
package main

import (
	"context"
	"fmt"

	"go.llib.dev/frameless/pkg/cli"
)

func main() {
	var mux cli.Mux

	mux.Handle("foo", FooCommand{})

	sub := mux.Sub("sub")
	sub.Handle("subcmd", SubCommand{})

	cli.Main(context.Background(), &mux)
}

type FooCommand struct {
	A string `flag:"the-a,a" default:"val"   desc:"this is flag A"`
	B bool   `flag:"the-b,b" default:"true"` // missing description
	C int    `flag:"c" required:"true"       desc:"this is flag C, not B"`
	D string `flag:"d" enum:"FOO,BAR,BAZ,"   desc:"this flag is an enum"`

	Arg    string `arg:"0" desc:"something something"`
	OthArg int    `arg:"1" default:"42"`

	// Dependency is a dependency of the FooCommand, which is populated though traditional dependency injection.
	Dependency string
}

func (cmd FooCommand) ServeCLI(w cli.Response, r *cli.Request) {
	fmt.Fprintln(w, "hello")
}

type SubCommand struct{}

func (cmd SubCommand) ServeCLI(w cli.Response, r *cli.Request) {
	w.Write([]byte("sub-cmd"))
}
Output:

Example (DependencyInjection)
package main

import (
	"context"

	"go.llib.dev/frameless/pkg/cli"
)

type CommandWithDependency struct {
	Flag1 string `flag:"flag1" default:"val" desc:"this is flag A"`
	Flag2 bool   `flag:"flag2" default:"true"`
	Flag3 int    `flag:"othflag" required:"true" desc:"this is flag C, not B"`

	Arg1 string `arg:"0" desc:"something something"`
	Arg2 int    `arg:"1" default:"42" desc:"something something else"`

	// Dependency is a dependency of the FooCommand, which is populated though traditional dependency injection.
	Dependency any
}

func (CommandWithDependency) ServeCLI(w cli.Response, r *cli.Request) {}

func main() {
	cli.Main(context.Background(), CommandWithDependency{
		Dependency: "important dependency that I need as part of the ServeCLI call",
	})
}
Output:

Index

Examples

Constants

View Source
const (
	// ExitCodeOK : Success
	ExitCodeOK = 0
	// ExitCodeError : General Error
	ExitCodeError = 1
	// ExitCodeBadRequest : Misuse of shell builtins or invalid command-line usage, often equated with a bad request.
	ExitCodeBadRequest = 2
)
View Source
const (
	ErrFlagMissing    errorkit.Error = "ErrFlagMissing"
	ErrFlagParseIssue errorkit.Error = "ErrFlagParseIssue"
	ErrFlagInvalid    errorkit.Error = "ErrFlagInvalid"

	ErrArgMissing      errorkit.Error = "ErrArgMissing"
	ErrArgParseIssue   errorkit.Error = "ErrArgParseIssue"
	ErrArgIndexInvalid errorkit.Error = "ErrArgIndexInvalid"

	ErrInvalidDefaultValue errorkit.Error = "ErrInvalidDefaultValue"
)

Variables

View Source
var EnumError = errorkit.UserError{
	Code:    "enum-error",
	Message: "invalid enumeration value",
}

Functions

func ConfigureHandler added in v0.276.0

func ConfigureHandler[H Handler](h H, path string, r *Request) (zero H, _ error)

func Main

func Main(ctx context.Context, h Handler)

func Usage added in v0.276.0

func Usage(h Handler, pattern string) (string, error)

Usage will generate a help usage message for a given handler on a given command request pattern/path.

Types

type ErrorWriter

type ErrorWriter interface {
	Stderr() io.Writer
}

type Handler

type Handler interface {
	ServeCLI(w Response, r *Request)
}

type HandlerFunc

type HandlerFunc func(w Response, r *Request)

func (HandlerFunc) ServeCLI

func (fn HandlerFunc) ServeCLI(w Response, r *Request)

type HelpSummary

type HelpSummary interface {
	// Summary returns a summary about the application
	//
	// TODO: Maybe ranem this to "Desc" as the tag "desc" is used for this purpose
	Summary() string
}

type HelpUsage added in v0.277.0

type HelpUsage interface {
	Usage(pattern string) (string, error)
}

type Multiplexer added in v0.276.0

type Multiplexer interface {
	Handle(pattern string, h Handler)
}

Multiplexer is an interface that, when implemented by a command, delegates the parsing of input arguments and options to the Handler in cli.Main.

If you want to create your own Mux, simply implement this interface in your structure.

type Mux

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

func (*Mux) Handle

func (m *Mux) Handle(pattern string, h Handler)

func (*Mux) ServeCLI

func (m *Mux) ServeCLI(w Response, r *Request)

func (*Mux) Sub

func (m *Mux) Sub(pattern string) *Mux

type Request

type Request struct {
	Args []string
	Body io.Reader
	// contains filtered or unexported fields
}

func (Request) Context

func (r Request) Context() context.Context

type Response

type Response interface {
	ExitCode(n int)
	io.Writer
}

type ResponseRecorder

type ResponseRecorder struct {
	Code int
	Out  bytes.Buffer
	Err  bytes.Buffer
}

func (*ResponseRecorder) ExitCode

func (rr *ResponseRecorder) ExitCode(n int)

func (*ResponseRecorder) Stdeout

func (rr *ResponseRecorder) Stdeout() io.Writer

func (*ResponseRecorder) Stderr

func (rr *ResponseRecorder) Stderr() (_ io.Writer)

func (*ResponseRecorder) Write

func (rr *ResponseRecorder) Write(p []byte) (n int, err error)

Directories

Path Synopsis
example
mux

Jump to

Keyboard shortcuts

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