lieut

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 22, 2024 License: MIT Imports: 12 Imported by: 5

README

lieut

Build Status Coverage Status Go Report Card Go Reference Latest Stable Version

Lieut, short for lieutenant, or "second-in-command" to a commander.

An opinionated, feature-limited, no external dependency, "micro-framework" for building command line applications in Go.

But why though?

In general, I personally don't like using frameworks... especially macro frameworks, and especially in Go.

I prefer using Go's extensive standard library, and when that doesn't quite provide what I'm looking for, I look towards smaller libraries that carry few (if any) other external dependencies and that integrate well with the standard library and its interfaces.

That being said, the flag package in the standard library leaves a lot to be desired, and unfortunately acts far less like a library and much more like a framework (it's library code that calls os.Exit()... 😕😖). It defines a lot of higher-level application behaviors than typically expected, and those can be a little surprising.

The goal of this project is to get some of those quirks out of the way (or at least work WITH them in ways that reduce the surprises in behavior) and reduce the typical "setup" code that a command line application needs, all while working with the standard library, to expand upon it's capabilities rather than locking your application into a tree of external/third-party dependencies.

"Wait, what? Why not just use 'x' or 'y'?" Don't worry, I've got you covered.

Project Status

This project is currently in "pre-release". The API may change. Use a tagged version or vendor this dependency if you plan on using it.

Features

  • Relies solely on the standard library.
  • Sub-command applications (app command, app othercommand).
  • Automatic handling of typical error paths.
  • Standardized output handling of application (and command) usage, description, help, and version..
  • Help flag (--help) handling, with actual user-facing notice (it shows up as a flag in the options list), rather than just handling it silently..
  • Version flag (--version) handling with a standardized output.
  • Global and sub-command flags with automatic merging.
  • Built-in signal handling (interrupt) with context cancellation.
  • Smart defaults, so there's less to configure.

Example

package main

import (
	"context"
	"flag"
	"fmt"
	"os"

	"github.com/Rican7/lieut"
)

func main() {
	do := func(ctx context.Context, arguments []string) error {
		_, err := fmt.Println(arguments)

		return err
	}

	app := lieut.NewSingleCommandApp(
		lieut.AppInfo{Name: "example"},
		do,
		flag.CommandLine,
		os.Stdout,
		os.Stderr,
	)

	exitCode := app.Run(context.Background(), os.Args[1:])

	os.Exit(exitCode)
}

For more examples, see the documentation.

How can I use this with another flag package?

pflag

If you want to use the github.com/spf13/pflag package (or one of its many forks), you'll just need to "merge" the global and sub-command flag-sets yourself... Like this:

globalFlags := flag.NewFlagSet("app", flag.ContinueOnError)
globalFlags.String("someglobalflag", "example", "an example global flag")

subCommandFlags := flag.NewFlagSet("subcommand", flag.ContinueOnError)
subCommandFlags.AddFlagSet(globalFlags) // Merge the globals into this command's flags
Other

Honestly, I haven't tried, but I'd imagine you just handle and parse the flags yourself before running the lieut app.

Wait, what? Why not just use "x" or "y"?

If you're reading this, you're probably thinking of an alternative solution or wondering why someone would choose this library over another. Well, I'm not here to convince you, but if you're curious, read on.

I've tried using many of the popular libraries (cobra, kingpin, urfave/cli, etc), and they all suffer from one of the following problems:

  • They're large in scope and attempt to solve too many problems, generically.
  • They use external/third-party dependencies.
  • They don't work well directly with the standard library.
  • They've been abandoned (practically, if not directly).
    • (This would be less of a concern if it weren't for the fact that they're LARGE in scope, so it's harder to just own yourself if the source rots).

If none of that matters to you, then that's fine, but they were enough of a concern for me to spend a few days extracting this package's behaviors from another project and making it reusable. 🙂

Documentation

Overview

Package lieut provides mechanisms to standardize command line app execution.

Lieut, short for lieutenant, or "second-in-command" to a commander.

An opinionated, feature-limited, no external dependency, "micro-framework" for building command line applications in Go.

Example (Minimal)
package main

import (
	"context"
	"flag"
	"fmt"
	"os"

	"github.com/Rican7/lieut"
)

func main() {
	do := func(ctx context.Context, arguments []string) error {
		_, err := fmt.Println(arguments)

		return err
	}

	app := lieut.NewSingleCommandApp(
		lieut.AppInfo{Name: "example"},
		do,
		flag.CommandLine,
		os.Stdout,
		os.Stderr,
	)

	exitCode := app.Run(context.Background(), os.Args[1:])

	os.Exit(exitCode)
}
Output:

Example (MultiCommand)
package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"time"

	"github.com/Rican7/lieut"
)

var multiAppInfo = lieut.AppInfo{
	Name:    "now",
	Summary: "An example CLI app to report the date and time",
	Usage:   "<command>... [options]...",
	Version: "v1.0.4",
}

var (
	timeZone       = "UTC"
	includeSeconds = false
	includeYear    = false
)

var location *time.Location

func main() {
	globalFlags := flag.NewFlagSet(multiAppInfo.Name, flag.ExitOnError)
	globalFlags.StringVar(&timeZone, "timezone", timeZone, "the timezone to report in")

	app := lieut.NewMultiCommandApp(
		multiAppInfo,
		globalFlags,
		os.Stdout,
		os.Stderr,
	)

	timeFlags := flag.NewFlagSet("time", flag.ExitOnError)
	timeFlags.BoolVar(&includeSeconds, "seconds", includeSeconds, "to include seconds")

	dateFlags := flag.NewFlagSet("date", flag.ExitOnError)
	dateFlags.BoolVar(&includeYear, "year", includeYear, "to include year")

	app.SetCommand(lieut.CommandInfo{Name: "time", Summary: "Show the time", Usage: "[options]"}, printTime, timeFlags)
	app.SetCommand(lieut.CommandInfo{Name: "date", Summary: "Show the date", Usage: "[options]"}, printDate, dateFlags)

	app.OnInit(validateGlobals)

	exitCode := app.Run(context.Background(), os.Args[1:])

	os.Exit(exitCode)
}

func validateGlobals() error {
	var err error

	location, err = time.LoadLocation(timeZone)

	return err
}

func printTime(ctx context.Context, arguments []string) error {
	format := "15:04"

	if includeSeconds {
		format += ":05"
	}

	_, err := fmt.Println(time.Now().Format(format))

	return err
}

func printDate(ctx context.Context, arguments []string) error {
	format := "01-02"

	if includeYear {
		format += "-2006"
	}

	_, err := fmt.Println(time.Now().Format(format))

	return err
}
Output:

Example (SingleCommand)
package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"strings"

	"github.com/Rican7/lieut"
)

var singleAppInfo = lieut.AppInfo{
	Name:    "sayhello",
	Summary: "An example CLI app to say hello to the given names",
	Usage:   "[option]... [names]...",
	Version: "v0.1-alpha",
}

func main() {
	flagSet := flag.NewFlagSet(singleAppInfo.Name, flag.ExitOnError)

	app := lieut.NewSingleCommandApp(
		singleAppInfo,
		sayHello,
		flagSet,
		os.Stdout,
		os.Stderr,
	)

	exitCode := app.Run(context.Background(), os.Args[1:])

	os.Exit(exitCode)
}

func sayHello(ctx context.Context, arguments []string) error {
	names := strings.Join(arguments, ", ")
	hello := fmt.Sprintf("Hello %s!", names)

	_, err := fmt.Println(hello)

	return err
}
Output:

Index

Examples

Constants

View Source
const (
	// DefaultCommandUsage defines the default usage string for commands.
	DefaultCommandUsage = "[arguments ...]"

	// DefaultParentCommandUsage defines the default usage string for commands
	// that have sub-commands.
	DefaultParentCommandUsage = "<command> [arguments ...]"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type AppInfo

type AppInfo struct {
	Name    string
	Summary string
	Usage   string
	Version string
}

AppInfo describes information about an app.

type CommandInfo

type CommandInfo struct {
	Name    string
	Summary string
	Usage   string
}

CommandInfo describes information about a command.

type Executor

type Executor func(ctx context.Context, arguments []string) error

Executor is a functional interface that defines an executable command.

It takes a context and arguments, and returns an error (if any occurred).

type Flags

type Flags interface {
	Parse(arguments []string) error
	Args() []string
	PrintDefaults()
	Output() io.Writer
	SetOutput(output io.Writer)
}

Flags defines an interface for command flags.

type MultiCommandApp

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

MultiCommandApp is a runnable application that has many commands.

func NewMultiCommandApp

func NewMultiCommandApp(info AppInfo, flags Flags, out io.Writer, errOut io.Writer) *MultiCommandApp

NewMultiCommandApp returns an initialized MultiCommandApp.

The provided flags are global/shared among the app's commands.

The provided flags should have ContinueOnError ErrorHandling, or else flag parsing errors won't properly be displayed/handled.

func (*MultiCommandApp) CommandNames

func (a *MultiCommandApp) CommandNames() []string

CommandNames returns the names of the set commands.

func (*MultiCommandApp) OnInit

func (a *MultiCommandApp) OnInit(init func() error)

OnInit takes an init function that is then called after initialization and before execution of a command.

func (*MultiCommandApp) PrintHelp

func (a *MultiCommandApp) PrintHelp(commandName string)

PrintHelp prints the help info to the app's error output.

It's exposed so it can be called or assigned to a flag set's usage function.

func (*MultiCommandApp) PrintUsage

func (a *MultiCommandApp) PrintUsage(commandName string)

PrintUsage prints the usage to the app's error output.

func (*MultiCommandApp) PrintUsageError

func (a *MultiCommandApp) PrintUsageError(commandName string, err error)

PrintUsageError prints a standardized usage error to the app's error output.

func (*MultiCommandApp) PrintVersion

func (a *MultiCommandApp) PrintVersion()

PrintVersion prints the version to the app's standard output.

func (*MultiCommandApp) Run

func (a *MultiCommandApp) Run(ctx context.Context, arguments []string) int

Run takes a context and arguments, runs the expected command, and returns an exit code.

If the init function or command Executor returns a StatusCodeError, then the returned exit code will match that of the value returned by StatusCodeError.StatusCode().

func (*MultiCommandApp) SetCommand

func (a *MultiCommandApp) SetCommand(info CommandInfo, exec Executor, flags Flags) error

SetCommand sets a command for the given info, executor, and flags.

It returns an error if the provided flags have already been used for another command (or for the globals).

The provided flags should have ContinueOnError ErrorHandling, or else flag parsing errors won't properly be displayed/handled.

type SingleCommandApp

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

SingleCommandApp is a runnable application that only has one command.

func NewSingleCommandApp

func NewSingleCommandApp(info AppInfo, exec Executor, flags Flags, out io.Writer, errOut io.Writer) *SingleCommandApp

NewSingleCommandApp returns an initialized SingleCommandApp.

The provided flags should have ContinueOnError ErrorHandling, or else flag parsing errors won't properly be displayed/handled.

func (*SingleCommandApp) OnInit

func (a *SingleCommandApp) OnInit(init func() error)

OnInit takes an init function that is then called after initialization and before execution of a command.

func (*SingleCommandApp) PrintHelp

func (a *SingleCommandApp) PrintHelp()

PrintHelp prints the help info to the app's error output.

It's exposed so it can be called or assigned to a flag set's usage function.

func (*SingleCommandApp) PrintUsage

func (a *SingleCommandApp) PrintUsage()

PrintUsage prints the usage to the app's error output.

func (*SingleCommandApp) PrintUsageError

func (a *SingleCommandApp) PrintUsageError(err error)

PrintUsageError prints a standardized usage error to the app's error output.

func (*SingleCommandApp) PrintVersion

func (a *SingleCommandApp) PrintVersion()

PrintVersion prints the version to the app's standard output.

func (*SingleCommandApp) Run

func (a *SingleCommandApp) Run(ctx context.Context, arguments []string) int

Run takes a context and arguments, runs the expected command, and returns an exit code.

If the init function or command Executor returns a StatusCodeError, then the returned exit code will match that of the value returned by StatusCodeError.StatusCode().

type StatusCodeError

type StatusCodeError interface {
	error

	// StatusCode returns the status code of the error, which can be used by an
	// app's execution error to know which status code to return.
	StatusCode() int
}

StatusCodeError represents an error that reports an associated status code.

func ErrWithStatusCode

func ErrWithStatusCode(err error, statusCode int) StatusCodeError

ErrWithStatusCode takes an error and a status code and returns a type that satisfies StatusCodeError.

Jump to

Keyboard shortcuts

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