README

zli is a Go library for writing CLI programs. It includes flag parsing, color escape codes, various helpful utility functions, and makes testing fairly easy. There's a little example at cmd/grep, which should give a decent overview of how actual programs look like.

Import as zgo.at/zli; API docs: https://pkg.go.dev/zgo.at/zli

Readme index: Utility functions · Flag parsing · Colours · Testing

Utility functions

zli.Errorf() and zli.Fatalf() work like fmt.Printf(), except that they print to stderr, prepend the program name, and always append a newline:

zli.Errorf("oh noes: %s", "u brok it")   // "progname: oh noes: u brok it"
zli.Fatalf("I swear it was %s", "Dave")  // "progname: I swear it was Dave" and exit 1

zli.F() is a small wrapper/shortcut around zli.Fatalf() which accepts an error and checks if it's nil first:

err := f()
zli.F(err)

For many programs it's useful to be able to read from stdin or from a file, depending on what arguments the user gave. With zli.InputOrFile() this is pretty easy:

fp, err := zli.InputOrFile("/a-file", false)  // Open a file.

fp, err := zli.InputOrFile("-", false)        // Read from stdin; can also use "" for stdin.
defer fp.Close()                              // No-op close on stdin.

The second argument controls if a reading from stdin... message should be printed to stderr, which is a bit better UX IMHO (how often have you typed grep foo and waited, only to realize it's waiting for stdin?). See Better UX when reading from stdin.

With zli.InputOrArgs() you can read arguments from stdin if it's an empty list:

args := zli.InputOrArgs(os.Args[1:], "\n", false)     // Split arguments on newline.
args := zli.InputOrArgs(os.Args[1:], "\n\t ", false)  // Or on spaces and tabs too.

With zli.Pager() you can pipe the contents of a reader $PAGER; this will just copy the contents to stdout if $PAGER isn't set or on other errors:

fp, _ := os.Open("/file")        // Display file in $PAGER.
zli.Pager(fp)

If you want to page output your program generates you can use zli.PagerStdout() to swap zli.Stdout to a buffer:

defer zli.PagerStdout()()               // Double ()()!
fmt.Fprintln(zli.Stdout, "page me!")    // Displayed in the $PAGER.

This does require that your program writes to zli.Stdout instead of os.Stdout, which is probably a good idea for testing anyway. See the Testing section.

You need to be a bit careful when calling Exit() explicitly, since that will exit immediately without running any defered functions. You have to either use a wrapper or call the returned function explicitly:

func main() { zli.Exit(run()) }

func run() int {
    defer zli.PagerStdout()()
    fmt.Fprintln(zli.Stdout, "XXX")
    return 1
}
func main() {
    runPager := zli.PagerStdout()
    fmt.Fprintln(zli.Stdout, "XXX")

    runPager()
    zli.Exit(1)
}

zli helpfully includes the go-isatty and GetSize() from x/crypto/ssh/terminal as they're so commonly used:

interactive := zli.IsTerminal(os.Stdout.Fd())  // Check if stdout is a terminal.
w, h, err := zli.TerminalSize(os.Stdout.Fd())  // Get terminal size.

Flag parsing

zli comes with a flag parser which, IMHO, gives a better experience than Go's flag package. See flag.markdown for some rationale on "why this and not stdlib flags?"

// Create new flags; normally you'd pass in os.Args here.
f := zli.NewFlags([]string{"example", "-vv", "-f=csv", "-a", "xx", "yy"})

// The first argument is the default and everything after that is the flag name
// with aliases.
var (
    verbose = f.IntCounter(0, "v", "verbose")   // Count the number of -v flags.
    exclude = f.StringList(nil, "e", "exclude") // Can appear more than once.
    all     = f.Bool(false, "a", "all")         // Regular bool.
    format  = f.String("", "f", "format")       // Regular string.
)

// Shift the first argument (i.e. os.Args[1]). Useful to get the "subcommand"
// name. This works before and after Parse().
switch f.Shift() {
case "help":
    // Run help
case "install":
    // Run install
case "": // os.Args wasn't long enough.
    // Error: need a command (or just print the usage)
default:
    // Error: Unknown command
}

// Parse the shebang!
err := f.Parse()
if err != nil {
    // Print error, usage.
}

// You can check if the flag was present on the CLI with Set(). This way you can
// distinguish between "was an empty value passed" (-format '') and "this flag
// wasn't on the CLI".
if format.Set() {
    fmt.Println("Format was set to", format.String())
}

// The IntCounter adds 1 for every time the -v flag is on the CLI.
if verbose.Int() > 1 {
    // ...Print very verbose info.
} else if verbose.Int() > 0 {
    // ...Print less verbose info.
}

// Just a bool!
fmt.Println("All:", all.Bool())

// Allow a flag to appear more than once.
fmt.Println("%s exclude patterns: %v", len(all.Strings()), all.Strings())

// f.Args is set to everything that's not a flag or argument.
fmt.Println("Remaining:", f.Args)

The flag format is as follows:

  • Flags can have a single - or two --, they're treated identical.

  • Arguments are after a space or =: -f v or -f=v.

  • Booleans can be grouped; -ab is the same as -a -b; this only works with a single - (--ab would be an error).

  • Positional arguments may appear anywhere; these are all identical: -a -b arg, arg -a -b, -a arg -b.


There is no automatic generation of a usage message; I find that much of the time you get a much higher quality by writing one manually.

That being said, with zli.Usage() you can apply some generic substitutions giving a format somewhat reminiscent to manpages:

UsageTrim      Trim leading/trailing whitespace, and ensure it ends with \n
UsageHeaders   Format headers in the form "^Name:" as bold and underline.
UsageFlags     Format flags (-v, --flag, --flag=foo) as underlined.

See the grep example.

Colors

You can add colors and some other text attributes to a string with zli.Colorf(), which returns a modified string with the terminal escape codes.

It won't do anything if zli.WantColor is false; this is disabled by default if the output isn't a terminal or NO_COLOR is set, but you can override it if the user sets --color=force or something.

zli.Colorln() is a convenience wrapper for fmt.Println(zli.Colorf(..)).

There are constants for the basic terminal attributes and 16-color pallete which may be combined freely by adding them together:

zli.Colorln("You're looking rather red", zli.Red)     // Apply a color.
zli.Colorln("A bold move", zli.Bold)                  // Or an attribute.
zli.Colorln("A bold move", zli.Red | zli.Bold)        // Or both.

To set a background color transform the color with the Bg() method:

zli.Colorln("Tomato", zli.Red.Bg())                   // Transform to background color.
zli.Colorln("Wow, such beautiful text",               // Can be combined.
    zli.Bold | zli.Red | zli.Green.Bg())

There are no pre-defined constants for the 256-color palette or true colors, you need to use Color256() and ColorHex() to create them; you can use the Bg() to transform them to a background color as well:

zli.Colorln("Contrast ratios is for suckers",         // 256 color.
    zli.Color256(56) | zli.Color256(99).Bg())

zli.Colorln("REAL men use TRUE color!",               // True color.
    zli.ColorHex("#fff") | zli.ColorHex("#00f").Bg())

See cmd/colortest/main.go for a little program to display and test colors.


You don't need to use the color codes, you can use the String() method to get the terminal escape codes:

fmt.Println(zli.Red|zli.Bold, "red!")                 // Print escape codes.
fmt.Println("and bold!", zli.Reset)

fmt.Printf("%sc%so%sl%so%sr%s\n", zli.Red, zli.Magenta, zli.Cyan, zli.Blue, zli.Yellow, zli.Reset)

Because the color is stored in an uint64 you can assign them to a constant:

const colorMatch = zli.Bold | zli.Red

This won't work if you use Color256() or ColorHex(); although you can get around this by constructing it all yourself:

// zli.Color256(99)
const color = zli.Bold | (zli.Color(99) << zli.ColorOffsetFg) | zli.ColorMode256

// zli.ColorHex("#ff7711").Bg(); can also use 1144831 directly instead of the
// bit shifts.
const color2 = zli.Bold | zli.Red | zli.ColorModeTrueBg |
               (zli.Color(0xff|0x77<<8|0x11<<16) << zli.ColorOffsetBg)

This creates a color stored as an int, shifts it to the correct location, and sets the flag to signal how to interpret it.

Do you really want to do this just to create a const instead of a `var? Probably not 😅

Testing

zli uses to zli.Stdin, zli.Stdout, zli.Stderr, and zli.Exit instead of the os.* variants for everything. You can swap this out with test variants with the zli.Test() function.

You can use these in your own program as well, if you want to test the output.

func TestX(t *testing.T) {
    exit, in, out, reset := Test()
    defer reset() // Reset everything back to the os.* functions.

    // Write something to stderr (a bytes.Buffer) and read the output.
    Error("oh noes!")
    fmt.Println(out.String()) // zli.test: oh noes!

    // Read from stdin.
    in.WriteString("Hello")
    fp, _ := InputOrFile("-", true)
    got, _ := ioutil.ReadAll(fp)
    fmt.Println(string(got)) // Hello

    out.Reset()

    et := func() {
        fmt.Fprintln(Stdout, "one")
        Exit(1)
        fmt.Fprintln(Stdout, "two")
    }

    // exit panics to ensure the regular control flow of the program is aborted;
    // to capture this run the function to be tested in a closure with
    // exit.Recover(), which will recover() from the panic and set the exit
    // code.
    func() {
        defer exit.Recover()
        et()
    }()
    // Helper to check the statis code, so you don't have to derefrence and cast
    // the value to int.
    exit.Want(t, 1)

    fmt.Println("Exit %d: %s\n", *exit, out.String()) // Exit 1: one

You don't need to use the zli.Test() function if you won't want to, you can just swap out stuff yourself as well:

buf := new(bytes.Buffer)
zli.Stderr = buf
defer func() { Stderr = os.Stderr }()

Error("oh noes!")
out := buf.String()
fmt.Printf("buffer has: %q\n", out) // buffer has: "zli.test: oh noes!\n"

zli.IsTerminal() and zli.TerminalSize() are variables, and can be swapped out as well:

save := zli.IsTerminal
zli.IsTerminal = func(uintptr) bool { return true }
defer func() { IsTerminal = save }()

Exit

A few notes on replacing zli.Exit() in tests: the difficulty with this is that os.Exit() will terminate the entire program, including the test, which is rarely what you want and difficult to test. You can replace zli.Exit with something like:

var code int
zli.Exit = func(c int) { code = c }
mayExit()
fmt.Println("exit code", code)

This works well enough for simple cases, but there's a big caveat with this; for example consider:

func mayExit() {
    err := f()
    if err != nil {
        zli.Error(err)
        zli.Exit(4)
    }

    fmt.Println("X")
}

With the above the program will continue after zli.Exit(); which is a different program flow from normal execution. A simpel way to fix it so to modify the function to explicitly call return`:

func mayExit() {
    err := f()
    if err != nil {
        zli.Error(err)
        zli.Exit(4)
        return
    }

    fmt.Println("X")
}

This still isn't quite the same, as callers of mayExit() in your program will still continue happily. It's also rather ugly and clunky.

To solve this you can replace zli.Exit with a function that panics and then recover that:

func TestFoo(t *testing.T) {
    var code int
    zli.Exit = func(c int) {
        code = c
        panic("zli.Exit")
    }

    func() {
        defer func() {
            r := recover()
            if r == nil {
                return
            }
        }()

        mayExit()
    }()

    fmt.Println("Exited with", code)
}

This will abort the program flow similar to os.Exit(), and the call to mayExit is wrapped in a function the test function itself will continue after the recover.

zli.TestExit takes care of all of this.

Expand ▾ Collapse ▴

Documentation

Index

Examples

Constants

const (
	ColorOffsetFg = 16
	ColorOffsetBg = 40
)

Offsets where foreground and background colors are stored.


const (
	// UsageTrim removes leading and trailing whitespace and appends a newline.
	//
	// This makes it easier to write usage strings without worrying too much
	// about leading/trailing whitespace, and with the trailing newline it's
	// easy to add a blank line between the usage and any error message
	// (fmt.Println if you wnat a blank line, fmt.Print if you don't).
	UsageTrim = 1

	// UsageHeaders formats headers in the form of:
	//
	//   Header:
	//
	// They must be at the start of the line and end with a :
	UsageHeaders = 2

	// UsageFlags formats flags in the form of:
	//
	//   -f
	//   -flag
	//   -flag=foo
	//   -flag=[foo]
	UsageFlags = 4
)

Formatting flags for Usage.


Variables

var (
	Exit   func(int) = os.Exit
	Stdin  io.Reader = os.Stdin
	Stdout io.Writer = os.Stdout
	Stderr io.Writer = os.Stderr
)

var ExitCode = 1

ExitCode is the exit code to use for Fatalf() and F()


var IsTerminal = func(fd uintptr) bool { return isatty.IsTerminal(fd) }

IsTerminal reports if this file descriptor is an interactive terminal.

TODO: this is a bit tricky now, as we can replace zli.Stdout with something else; checking os.Stdout may not be correct in those cases.


var TerminalSize = func(fd uintptr) (width, height int, err error) { return terminal.GetSize(int(fd)) }

TerminalSize gets the dimensions of the given terminal.


var WantColor = func() bool {
	_, ok := os.LookupEnv("NO_COLOR")
	return os.Getenv("TERM") != "dumb" && isatty.IsTerminal(os.Stdout.Fd()) && !ok
}()

WantColor indicates if the program should output any colors. This is automatically set from from the output terminal and NO_COLOR environment variable.

You can override this if the user sets "--color=force" or the like.

TODO: maybe expand this a bit with WantMonochrome or some such, so you can still output bold/underline/reverse text for people who don't want colors.


Functions

func Colorf

func Colorf(text string, c Color) string

Colorf applies terminal escape codes on the text.

func Colorln

func Colorln(text string, c Color)

Colorln prints colorized output.

func DeColor

func DeColor(text string) string

DeColor removes ANSI color escape sequences from a string.

func EraseLine

func EraseLine()

ErasesLine erases the entire line and puts the cursor at the start of the line.

func Errorf

func Errorf(s interface{}, args ...interface{})

Error prints an error message to stderr prepended with the program name and with a newline appended.

func F

func F(err error)

F prints the err.Error() to stderr with Errorf() and exits, but it won't do anything if the error is nil.

func Fatalf

func Fatalf(s interface{}, args ...interface{})

Fatalf is like Errorf(), but will exit with a code of 1.

func InputOrArgs

func InputOrArgs(args []string, sep string, quiet bool) ([]string, error)

InputOrArgs reads arguments separated by sep from stdin if args is empty, or returns args unmodified if it's not.

The argument are split on newline; the following are all identical:

prog foo bar
printf "foo\nbar\n" | prog

prog 'foo bar' 'x y'
printf "foo bar\nx y\n" | prog

It will print a message to stderr notifying the user it's reading from stdin if the terminal is interactive and quiet is false. See: https://www.arp242.net/read-stdin.html

func InputOrFile

func InputOrFile(path string, quiet bool) (io.ReadCloser, error)

InputOrFile will return a reader connected to stdin if path is "" or "-", or open a path for any other value.

It will print a message to stderr notifying the user it's reading from stdin if the terminal is interactive and quiet is false. See: https://www.arp242.net/read-stdin.html

func Pager

func Pager(text io.Reader)

Pager pipes the content of text to $PAGER, or prints it to stdout of this fails.

func PagerStdout

func PagerStdout() func()

PagerStdout replaces Stdout with a buffer and pipes the content of it to $PAGER.

The typical way to use this is at the start of a function like so:

defer zli.PageStdout()()

You need to be a bit careful when calling Exit() explicitly, since that will exit immediately without running any defered functions. You have to either use a wrapper or call the returned function explicitly.

func Program

func Program() string

Program gets the program name from argv.

func ReplaceLine

func ReplaceLine(a ...interface{})

ReplaceLine replaces the current line.

func ReplaceLinef

func ReplaceLinef(s string, a ...interface{})

ReplaceLinef replaces the current line.

func Usage

func Usage(opts int, text string) string

Usage applies some formatting to a usage message. See the Usage* constants.

Types

type Color

type Color uint64

Color is a set of attributes to apply; the attributes are stored as follows:

                                     fg true, 256, 16 color mode ─┬──┐
                                  bg true, 256, 16 color mode ─┬─┐│  │
                                                               │ ││  │┌── error parsing hex color
   ┌───── bg color ────────────┐ ┌───── fg color ────────────┐ │ ││  ││┌─ term attr
   v                           v v                           v v vv  vvv         v
0b 0000_0000 0000_0000 0000_0000 0000_0000 0000_0000 0000_0000 0000_0000 0000_0000

The terminal attributes are bold, underline, etc. are stored as a bitmask. The error flag signals there was an error parsing a hex color with ColorHex().

The colors are stored separately for the background and foreground, the colors are applied depending on the values of the color mode bitmasks: if it's blank, nothing is applied.

The biggest advantage of storing it like this is that we can can use a single variable/function parameter to represent all terminal attributes, for example:

var colorMatch = zli.Bold | zli.Red | zli.ColorHex("#f71").Bg()
fmt.Println(zli.Colorf("foo", colorMatch))

Which IMHO gives a rather nicer API than using a slice or composing the colors with functions or some such.

If you want to be really savvy about it then you can store it as a constant too:

const colorMatch = zli.Bold | zli.Red | (zli.Color(0xff|0x77<<8|0x11<<16) << zli.ColorOffsetBg) | zli.ColorModeTrueBg

This creates 24bit color stored as an int (0xff, 0x77, 0x11 is the same as "#ff7711", or "#f71" in short notation) shifts it to the correct location, and sets the flag so the background is read as a 24bit color.

Example

Code:

package main

import (
	"fmt"
	"zgo.at/zli"
)

func main() {
	zli.Colorln("You're looking rather red", zli.Red) // Apply a color.
	zli.Colorln("A bold move", zli.Bold)              // Or an attribute.
	zli.Colorln("Tomato", zli.Red.Bg())               // Transform to background color.

	zli.Colorln("Wow, such beautiful text", // Can be combined.
		zli.Bold|zli.Underline|zli.Red|zli.Green.Bg())

	zli.Colorln("Contrast ratios is for suckers", // 256 color
		zli.Color256(56)|zli.Color256(99).Bg())

	zli.Colorln("REAL men use TRUE color!", // True color
		zli.ColorHex("#fff")|zli.ColorHex("#00f").Bg())

	fmt.Println(zli.Red|zli.Bold, "red!") // Set colors "directly"
	fmt.Println("and bold!", zli.Reset)
	fmt.Printf("%sc%so%sl%so%sr%s\n", zli.Red, zli.Magenta, zli.Cyan, zli.Blue, zli.Yellow, zli.Reset)

}
�[31mYou're looking rather red�[0m
�[1mA bold move�[0m
�[41mTomato�[0m
�[1;4;31;42mWow, such beautiful text�[0m
�[38;5;56;48;5;99mContrast ratios is for suckers�[0m
�[38;2;255;255;255;48;2;0;0;255mREAL men use TRUE color!�[0m
�[1;31m red!
and bold! �[0m
�[31mc�[35mo�[36ml�[34mo�[33mr�[0m
const (
	Reset Color = 0
	Bold  Color = 1 << (iota - 1)
	Faint
	Italic
	Underline
	BlinkSlow
	BlinkRapid
	ReverseVideo
	Concealed
	CrossedOut
)

Basic terminal attributes.

const (
	ColorMode16 Color = ColorError << (iota + 1)
	ColorMode256
	ColorModeTrue

	ColorMode16Bg
	ColorMode256Bg
	ColorModeTrueBg
)

Color modes.

const (
	Black Color = (iota << ColorOffsetFg) | ColorMode16
	Red
	Green
	Yellow
	Blue
	Magenta
	Cyan
	White
	BrightBlack
	BrightRed
	BrightGreen
	BrightYellow
	BrightBlue
	BrightMagenta
	BrightCyan
	BrightWhite
)

First 16 colors.

const ColorError Color = CrossedOut << 1

ColorError signals there was an error in parsing a color hex attribute.

func Color256

func Color256(n uint8) Color

Color256 created a new 256-mode color.

The first 16 (starting at 0) are the same as the color names (Black, Red, etc.) 16 to 231 are various colors. 232 to 255 are greyscale tones.

The 16-231 colors should always be identical on every display (unlike the basic colors, whose exact color codes are undefined and differ per implementation).

See ./cmd/colortest for a little CLI to display the colors.

func ColorHex

func ColorHex(hex string) Color

ColorHex gets a 24-bit "true color" from a hex string such as "#f44" or "#ff4444". The leading "#" is optional.

Parsing errors are signaled with by setting the ColorError flag, which Colorf() shows as "(zli.Color ERROR invalid hex color)".

func (Color) Bg

func (c Color) Bg() Color

Bg returns the background variant of this color. If doesn't do anything if this is already a background color.

func (Color) String

func (c Color) String() string

String gets the escape sequence for this color code.

This will always return an empty string if WantColor is false or if the error flag is set.

You can use this to set colors directly with fmt.Print. You can reset the the default with zli.Reset:

fmt.Println(zli.Red|zli.Bold, "red!") // Set colors "directly"
fmt.Println("and bold!", zli.Reset)

fmt.Printf("%sc%so%sl%so%sr%s\n", zli.Red, zli.Magenta, zli.Cyan, zli.Blue, zli.Yellow, zli.Reset)

type ErrFlagDouble

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

ErrFlagDouble is used when a flag is given more than once.

func (ErrFlagDouble) Error

func (e ErrFlagDouble) Error() string

type ErrFlagInvalid

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

ErrFlagInvalid is used when a flag has an invalid syntax (e.g. "no" for an int flag).

func (ErrFlagInvalid) Error

func (e ErrFlagInvalid) Error() string

func (ErrFlagInvalid) Unwrap

func (e ErrFlagInvalid) Unwrap() error

type ErrFlagUnknown

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

ErrFlagUnknown is used when the flag parsing encounters unknown flags.

func (ErrFlagUnknown) Error

func (e ErrFlagUnknown) Error() string

type Flags

type Flags struct {
	Program string   // Program name.
	Args    []string // List of arguments, after parsing this will be reduces to non-flags.
	// contains filtered or unexported fields
}
Example

Code:

package main

import (
	"fmt"
	"zgo.at/zli"
)

func main() {
	// Create new flags from os.Args.
	f := zli.NewFlags([]string{"example", "-vv", "-f=csv", "-a", "xx", "yy"})

	// Add a string, bool, and "counter" flag.
	var (
		verbose = f.IntCounter(0, "v", "verbose")
		all     = f.Bool(false, "a", "all")
		format  = f.String("", "f", "format")
	)

	// Shift the first argument (i.e. os.Args[1], if any, empty string if there
	// isn't). Useful to get the "subcommand" name. This works before and after
	// Parse().
	switch f.Shift() {
	case "help":
		// Run help
	case "install":
		// Run install
	case "":
		// Error: need a command (or just print the usage)
	default:
		// Error: Unknown command
	}

	// Parse the shebang!
	err := f.Parse()
	if err != nil {
		// Print error, usage.
	}

	// You can check if the flag was present on the CLI with Set(). This way you
	// can distinguish between "was an empty value passed" // (-format '') and
	// "this flag wasn't on the CLI".
	if format.Set() {
		fmt.Println("Format was set to", format.String())
	}

	// The IntCounter adds 1 for every time the -v flag is on the CLI.
	if verbose.Int() > 1 {
		// ...Print very verbose info.
	} else if verbose.Int() > 0 {
		// ...Print less verbose info.
	}

	// Just a bool!
	fmt.Println("All:", all.Bool())

	// f.Args is set to everything that's not a flag or argument.
	fmt.Println("Remaining:", f.Args)

}
Format was set to csv
All: true
Remaining: [xx yy]

func NewFlags

func NewFlags(args []string) Flags

NewFlags creates a new Flags from os.Args.

func (*Flags) Bool

func (f *Flags) Bool(def bool, name string, aliases ...string) flagBool

func (*Flags) Float64

func (f *Flags) Float64(def float64, name string, aliases ...string) flagFloat64

func (*Flags) Int

func (f *Flags) Int(def int, name string, aliases ...string) flagInt

func (*Flags) Int64

func (f *Flags) Int64(def int64, name string, aliases ...string) flagInt64

func (*Flags) IntCounter

func (f *Flags) IntCounter(def int, name string, aliases ...string) flagIntCounter

func (*Flags) Parse

func (f *Flags) Parse() error

func (*Flags) Shift

func (f *Flags) Shift() string

Shift a value from the argument list.

func (*Flags) String

func (f *Flags) String(def, name string, aliases ...string) flagString

func (*Flags) StringList

func (f *Flags) StringList(def []string, name string, aliases ...string) flagStringList

type TestExit

type TestExit int

TestExit records the exit code and aborts the normal program execution. It's intended to test exit codes in a program.

The Exit() method call is a replacement for zli.Exit:

exit := TestExit(-1)
Exit = exit.Exit
defer func() { Exit = os.Exit }()

This can be recovered like so:

func() {
    defer exit.Recover()
    Fatal("oh noes!")
}()
fmt.Println("Exit", exit)

The function wrapper is needed so that the test function itself doesn't get aborted.

func Test

func Test() (exit *TestExit, in, out *bytes.Buffer, reset func())

Test replaces Stdin, Stdout, Stderr, and Exit for testing.

The code points to the latest zli.Exit() return code;

func (*TestExit) Exit

func (t *TestExit) Exit(c int)

Exit sets TestExit to the given status code and panics with itself.

func (*TestExit) Recover

func (t *TestExit) Recover()

Recover any panics where the argument is this TestExit instance. it will re-panic on any other errors (including other TestExit instances).

func (*TestExit) Want

func (t *TestExit) Want(tt *testing.T, c int)

Want checks that the recorded exit code matches the given code and issues a t.Error() if it doesn't.

Directories

Path Synopsis
cmd/colortest Command colortest prints an overview of colors for testing.
cmd/grep Command grep is a simple grep implementation for demo purposes.
internal/isatty Package isatty implements interface to isatty
internal/terminal