flargs

package module
v1.3.2 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2024 License: MIT Imports: 10 Imported by: 6

README

Go Flargs

go flargs

Flargs is a simple and lightweight framework for building command-line programs with the following design goals:

  1. Is testable, providing abstractions around stdin, stdout, stderr, etc
  2. Decouples the act of parsing arguments from the act of consuming inputs
  3. Is chainable and composable, allowing for arbitrarily large and complex apps

Flargs conceives of 3 lifecycles, cleanly seperated:

  1. Parsing Flags and Args (flarging). This is the act of parsing arguments and flags into a custom structure (a flarg). The step allows no access to the environment.
  2. Loading flargs. This step allows access to an environment is allows further processing and validating.
  3. Execution. This is where your command is run. It runs against the object you created in step 1 and 2.

Flargs is composed of 3 basic components:

Konf

This is your custom object which your app will run against. It takes any shape you want, but you must embed flargs.StateMachine:

type myApiClient struct {
    hostname string
    port int
    path string
    flargs.stateMachine
}

Because you've embedded flargs.StateMachine, the struct will automatically implement this interface:

type Flarger[T any] interface {
	Parse([]string) ([]string, error)
	Load(*Environment) error
    Run(*Environment) error
}

But you will want to define at least one of these on your own to get any interesting behaviour.

Environment

An execution environment representing all the inputs and outputs a CLI should need.

type Environment struct {
	InputStream  io.ReadWriter
	OutputStream io.ReadWriter
	ErrorStream  io.ReadWriter
	Randomness   io.Reader
	Filesystem   fs.FS
	Variables    map[string]string
}

This object is injected using dependency injection. Your CLI must use it for all i/o. So:

// badly behaved ☹ don't do it
if os.Getenv("USER") == "sam" {
	fmt.Println("Sam, I am")
}

// well behaved ☺ this is the way
if env.Variables["USER"] == "sam" {
	fmt.Fprintln(env.OutputStream, "Sam, I am")
}

Command

A Command is a Konf plus an Environment, along with a way to run the former against the latter. It has Pipe() for composability and a handful of helper methods.

type Command[T any] struct {
	Env     *Environment
	runFunc RunFunc[T]
}

func (com1 Command[T]) Pipe(conf1 T, env2 *Environment) error {
	...
}
func (c Command) ParseAndLoad(args []string) error {
	...
}

Getting Started

A simple hello-world program that allows you to swap "world" for something else might look like this:

import (
    "github.com/sean9999/go-flargs"
)

//  our input structure. we only care about one value: name
type helloConf struct {
    name string
    flargs.StateMachine
}

//  get arg, set name
func (c *helloConf) Parse(args []string) error {
    if len(args) > 1 {
        return errors.New("too many args")
    }
    if len(args) == 1 {
        c.name = args[0]
    }
    c.name = "world"
    return nil
}

//  say hello
func (c *helloConf) Run(env *Environment) error {
    fmt.Fprintf(env.OutputStream, "hello %s", c.name)
}


conf := new(helloConf)
env := flargs.NewCLIEnvironment();
cmd := flargs.NewCommand(env, conf)
cmd.Run()

This might look pretty verbose for a simple CLI. But we now have a hermetic app that can be easily tested. It can grow in complexity without extra overhead. To test, we might do this:

func TestNewCommand_hello(t *testing.T) {

    conf := new(helloConf)

    //  run command in testing mode
	env := flargs.NewTestingEnvironment(nil)
	cmd := flargs.NewCommand(env, conf)
    cmd.Parse([]string{"robin"})
	cmd.Run(conf)

    //  expected output
    want := "hello, robin"

    //  actual output
	got := env.GetOutput()

    if want != got.String() {
        t.Errorf("wanted %q but got %q", want, string(got))
    }
}

Documentation

Overview

Flargs provides pleasant, fragrant, parsimonious parsing of flags and arguments, resulting in clean, testable, hermetic apps that provide feasible breeze and are easy to reason about.

Index

Constants

View Source
const (
	ExitCodeSuccess ExitCode = iota
	ExitCodeGenericError
	ExitCodeMisuseOfBuiltIns
	ExitCodeCannotExecute   = iota + 123
	ExitCodeCommandNotFound // Command not found
	ExitCodeInvalidArgumentToExit
	ExitCodeFatalErrorSignal1
	ExitCodeFatalErrorSignal2 // Ctrl-C was pressed
	ExitCodeFatalErrorSignal3
	ExitCodeFatalErrorSignal4
	ExitCodeFatalErrorSignal5
	ExitCodeFatalErrorSignal6
	ExitCodeFatalErrorSignal7
	ExitCodeFatalErrorSignal8
	ExitCodeFatalErrorSignal9
)

Variables

This section is empty.

Functions

func Pipe added in v1.1.0

func Pipe(f1 Command, f2 Command) (int64, error)

Pipe pipes one Command to another

Types

type Command

type Command struct {
	Flarger
	*Environment
}

a Command is a Flarger with an Environment

func NewCommand

func NewCommand(fl Flarger, env *Environment) Command

creates a Command

func (Command) Load added in v1.0.0

func (k Command) Load() error

Load processes the flarg configuration in the context of an Environment

func (Command) LoadAndRun added in v1.1.0

func (k Command) LoadAndRun() error

LoadAndRun combines Command.Load and Command.Run

func (Command) ParseAndLoad added in v1.1.0

func (k Command) ParseAndLoad(args []string) error

combines [Command.Parse] and Command.Load

func (Command) Run

func (k Command) Run() error

Run runs Flarger.Run in the context of an Environment

type Environment

type Environment struct {
	InputStream  io.ReadWriter
	OutputStream io.ReadWriter
	ErrorStream  io.ReadWriter
	Randomness   rand.Source
	Filesystem   fs.FS
	Variables    map[string]string
}

Enviroment is an execution environment for a Command. In the context of a CLI, these would be os.StdIn, os.StdOut, etc. In the context of a test-suite, you can use bytes.Buffer and fstest.MapFS. For benchmarking, you can use a NullDevice.

func NewCLIEnvironment

func NewCLIEnvironment(baseDir string) *Environment

NewCLIEnvironment produces an Environment suitable for a CLI. It's a helper function with sane defaults.

func NewNullEnvironment added in v1.1.0

func NewNullEnvironment() *Environment

func NewTestingEnvironment

func NewTestingEnvironment(randomnessProvider rand.Source) *Environment

NewTestingEnvironment produces an Environment suitable for testing. Pass in a "randomnessProvider" that offers a level of determinism that works for you. For good ole fashioned regular randomness, pass in rand.Reader If your program doesn't use randomness, just pass in nil.

func (Environment) GetError added in v0.1.2

func (e Environment) GetError() []byte

func (Environment) GetInput added in v0.1.2

func (e Environment) GetInput() []byte

func (Environment) GetOutput added in v0.1.2

func (e Environment) GetOutput() []byte

type ExitCode added in v0.1.2

type ExitCode uint8

func (ExitCode) Error added in v1.1.0

func (ec ExitCode) Error() string

type FlargError

type FlargError struct {
	ExitCode
	UnderlyingError error
}

func NewFlargError

func NewFlargError(exitcode ExitCode, underlying error) *FlargError

func (*FlargError) Error

func (fe *FlargError) Error() string

type Flarger added in v1.0.0

type Flarger interface {
	Parse([]string) error
	Load(*Environment) error
	Run(*Environment) error
}

a Flarger is a custom object that represents the state and functionality of your command

type NullDevice added in v1.1.0

type NullDevice struct {
	io.Writer
}

func (NullDevice) Open added in v1.1.0

func (b NullDevice) Open(_ string) (fs.File, error)

func (NullDevice) Read added in v1.1.0

func (b NullDevice) Read(_ []byte) (int, error)

type Phase added in v1.1.0

type Phase uint8

Phase represents lifecycle phases

const (
	Uninitialized Phase = iota
	Parsing
	Loading
	Running
	Ran
)

type StateMachine added in v1.1.0

type StateMachine struct {
	RemainingArgs []string
	Phase         Phase
}

StateMachine implements Flarger it provides basic functionality and default methods allowing you to not bother writing them if you don't need them

func (*StateMachine) Load added in v1.1.0

func (s *StateMachine) Load(_ *Environment) error

no-op. This will run if you don't define Load() in your konf.

func (*StateMachine) Parse added in v1.1.0

func (s *StateMachine) Parse(a []string) error

no-op. This will run if you don't define Parse() in your konf.

func (*StateMachine) Run added in v1.1.0

func (s *StateMachine) Run(_ *Environment) error

no-op. This will run if you don't define Run() in your konf.

Directories

Path Synopsis
cmd
kat
cmd
cmd
cmd
cmd

Jump to

Keyboard shortcuts

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