• Home
  • History
  • Annotate
Name Date Size #Lines LOC

..03-May-2022-

.github/H15-Dec-2021-21

cmd/H15-Dec-2021-450350

.gitignoreH A D15-Dec-202140 32

LICENSEH A D15-Dec-20211.1 KiB2217

README.mdH A D15-Dec-202113 KiB446336

color.goH A D15-Dec-20219.6 KiB350233

color_test.goH A D15-Dec-20215 KiB185139

csi.goH A D15-Dec-2021426 2011

flag.goH A D15-Dec-202110.8 KiB462359

flag.markdownH A D15-Dec-20212.3 KiB6242

flag_test.goH A D15-Dec-202115.2 KiB623502

go.modH A D15-Dec-202189 63

go.sumH A D15-Dec-2021416 54

no_term.goH A D15-Dec-2021478 167

run-ciH A D15-Dec-202172 63

term.goH A D15-Dec-20211.9 KiB6737

test.goH A D15-Dec-20212 KiB9345

test_test.goH A D15-Dec-20211,013 6554

usage.goH A D15-Dec-20211.8 KiB7942

usage_test.goH A D15-Dec-20211,010 5948

zli.goH A D15-Dec-20214.7 KiB205141

zli_test.goH A D15-Dec-20216.3 KiB297241

README.md

1zli is a Go library for writing CLI programs. It includes flag parsing, color
2escape codes, various helpful utility functions, and makes testing fairly easy.
3There's a little example at [cmd/grep](cmd/grep), which should give a decent
4overview of how actual programs look like.
5
6Import as `zgo.at/zli`; API docs: https://godocs.io/zgo.at/zli
7
8Other packages:
9
10- [zgo.at/termtext](https://github.com/arp242/termtext) – align and wrap text.
11- [zgo.at/acidtab](https://github.com/arp242/acidtab)   – print text in tables.
12- [zgo.at/termfo](https://github.com/arp242/termfo)     – read and use terminfo.
13
14**Readme index**:
15[Utility functions](#utility-functions) ·
16[Flag parsing](#flag-parsing) ·
17[Colors](#colors) ·
18[Testing](#testing)
19
20
21### Utility functions
22
23`zli.Errorf()` and `zli.Fatalf()` work like `fmt.Printf()`, except that they
24print to stderr, prepend the program name, and always append a newline:
25
26```go
27zli.Errorf("oh noes: %s", "u brok it")   // "progname: oh noes: u brok it"
28zli.Fatalf("I swear it was %s", "Dave")  // "progname: I swear it was Dave" and exit 1
29```
30
31`zli.F()` is a small wrapper/shortcut around `zli.Fatalf()` which accepts an
32error and checks if it's `nil` first:
33
34```go
35err := f()
36zli.F(err)
37```
38
39For many programs it's useful to be able to read from stdin or from a file,
40depending on what arguments the user gave. With `zli.InputOrFile()` this is
41pretty easy:
42
43```go
44fp, err := zli.InputOrFile("/a-file", false)  // Open a file.
45
46fp, err := zli.InputOrFile("-", false)        // Read from stdin; can also use "" for stdin.
47defer fp.Close()                              // No-op close on stdin.
48```
49
50The second argument controls if a `reading from stdin...` message should be
51printed to stderr, which is a bit better UX IMHO (how often have you typed `grep
52foo` and waited, only to realize it's waiting for stdin?) See [Better UX when
53reading from stdin][stdin].
54
55With `zli.InputOrArgs()` you can read arguments from stdin if it's an empty
56list:
57
58```go
59args := zli.InputOrArgs(os.Args[1:], "\n", false)     // Split arguments on newline.
60args := zli.InputOrArgs(os.Args[1:], "\n\t ", false)  // Or on spaces and tabs too.
61```
62
63[stdin]: https://www.arp242.net/read-stdin.html
64
65`zli.Pager()` pipes the contents of a reader `$PAGER`. It will copy the contents
66to stdout if `$PAGER` isn't set or on other errors:
67
68```go
69fp, _ := os.Open("/file")        // Display file in $PAGER.
70zli.Pager(fp)
71```
72
73If you want to page output your program generates you can use
74`zli.PagerStdout()` to swap `zli.Stdout` to a buffer:
75
76```go
77defer zli.PagerStdout()()               // Double ()()!
78fmt.Fprintln(zli.Stdout, "page me!")    // Displayed in the $PAGER.
79```
80
81This does require that your program writes to `zli.Stdout` instead of
82`os.Stdout`, which is probably a good idea for testing anyway. See the
83[Testing](#testing) section.
84
85You need to be a bit careful when calling `Exit()` explicitly, since that will
86exit immediately without running any defered functions. You have to either use a
87wrapper or call the returned function explicitly:
88
89```go
90func main() { zli.Exit(run()) }
91
92func run() int {
93    defer zli.PagerStdout()()
94    fmt.Fprintln(zli.Stdout, "XXX")
95    return 1
96}
97```
98
99```go
100func main() {
101    runPager := zli.PagerStdout()
102    fmt.Fprintln(zli.Stdout, "XXX")
103
104    runPager()
105    zli.Exit(1)
106}
107```
108
109zli helpfully includes the [go-isatty][isatty] and `GetSize()` from
110[x/crypto/ssh/terminal][ssh] as they're so commonly used:
111
112```go
113interactive := zli.IsTerminal(os.Stdout.Fd())  // Check if stdout is a terminal.
114w, h, err := zli.TerminalSize(os.Stdout.Fd())  // Get terminal size.
115```
116
117[isatty]: https://github.com/mattn/go-isatty/
118[ssh]: https://godoc.org/golang.org/x/crypto/ssh/terminal#GetSize
119
120
121### Flag parsing
122
123zli comes with a flag parser which, IMHO, gives a better experience than Go's
124`flag` package. See [flag.markdown](/flag.markdown) for some rationale on "why
125this and not stdlib flags?"
126
127```go
128// Create new flags; normally you'd pass in os.Args here.
129f := zli.NewFlags([]string{"example", "-vv", "-f=csv", "-a", "xx", "yy"})
130
131// The first argument is the default and everything after that is the flag name
132// with aliases.
133var (
134    verbose = f.IntCounter(0, "v", "verbose")   // Count the number of -v flags.
135    exclude = f.StringList(nil, "e", "exclude") // Can appear more than once.
136    all     = f.Bool(false, "a", "all")         // Regular bool.
137    format  = f.String("", "f", "format")       // Regular string.
138)
139
140// Shift the first argument (i.e. os.Args[1]). Useful to get the "subcommand"
141// name. This works before and after Parse().
142switch f.Shift() {
143case "help":
144    // Run help
145case "install":
146    // Run install
147case "": // os.Args wasn't long enough.
148    // Error: need a command (or just print the usage)
149default:
150    // Error: Unknown command
151}
152
153// Parse the shebang!
154err := f.Parse()
155if err != nil {
156    // Print error, usage.
157}
158
159// You can check if the flag was present on the CLI with Set(). This way you can
160// distinguish between "was an empty value passed" (-format '') and "this flag
161// wasn't on the CLI".
162if format.Set() {
163    fmt.Println("Format was set to", format.String())
164}
165
166// The IntCounter adds 1 for every time the -v flag is on the CLI.
167if verbose.Int() > 1 {
168    // ...Print very verbose info.
169} else if verbose.Int() > 0 {
170    // ...Print less verbose info.
171}
172
173// Just a bool!
174fmt.Println("All:", all.Bool())
175
176// Allow a flag to appear more than once.
177fmt.Println("%s exclude patterns: %v", len(all.Strings()), all.Strings())
178
179// f.Args is set to everything that's not a flag or argument.
180fmt.Println("Remaining:", f.Args)
181```
182
183The flag format is as follows:
184
185- Flags can have a single `-` or two `--`, they're treated identical.
186
187- Arguments are after a space or `=`: `-f v` or `-f=v`.
188
189- Booleans can be grouped; `-ab` is the same as `-a -b`; this only works with a
190  single `-` (`--ab` would be an error).
191
192- Positional arguments may appear anywhere; these are all identical:
193  `-a -b arg`, `arg -a -b`, `-a arg -b`.
194
195---
196
197There is no automatic generation of a usage message; I find that much of the
198time you get a much higher quality by writing one manually. It does provide
199`zli.Usage()` you can apply some generic substitutions giving a format somewhat
200reminiscent of manpages:
201
202    UsageTrim      Trim leading/trailing whitespace, and ensure it ends with \n
203    UsageHeaders   Format headers in the form "^Name:" as bold and underline.
204    UsageFlags     Format flags (-v, --flag, --flag=foo) as underlined.
205
206See the grep example.
207
208### Colors
209
210You can add colors and some other text attributes to a string with
211`zli.Colorize()`, which returns a modified string with the terminal escape codes,
212ending with reset.
213
214It won't do anything if `zli.WantColor` is `false`; this is disabled by default
215if the output isn't a terminal or `NO_COLOR` is set, but you can override it if
216the user sets `--color=force` or something.
217
218`zli.Colorln()` and `zli.Colorf()` are convenience wrappers for `fmt.Println()`
219and `fmt.Printf()` with colors.
220
221There are constants for the basic terminal attributes and 16-color palette which
222may be combined freely by adding them together:
223
224```go
225zli.Colorln("You're looking rather red", zli.Red)     // Apply a color.
226zli.Colorln("A bold move", zli.Bold)                  // Or an attribute.
227zli.Colorln("A bold move", zli.Red | zli.Bold)        // Or both.
228```
229
230To set a background color transform the color with the `Bg()` method:
231
232```go
233zli.Colorln("Tomato", zli.Red.Bg())                   // Transform to background color.
234zli.Colorln("Wow, such beautiful text",               // Can be combined.
235    zli.Bold | zli.Red | zli.Green.Bg())
236```
237
238There are no pre-defined constants for the 256-color palette or true colors, you
239need to use `Color256()` and `ColorHex()` to create them; you can use the `Bg()`
240to transform them to a background color as well:
241
242```go
243zli.Colorln("Contrast ratios is for suckers",         // 256 color.
244    zli.Color256(56) | zli.Color256(99).Bg())
245
246zli.Colorln("REAL men use TRUE color!",               // True color.
247    zli.ColorHex("#fff") | zli.ColorHex("#00f").Bg())
248```
249
250With `Brighten()` you can change the brightness of a color:
251
252```go
253zli.Colorln("Brighter! BRIGHTER!", zli.Color256(99).Brighten(1))
254zli.Colorln("Dim the lights.              // Negative values darken.
255    zli.ColorHex("#655199").Brighten(-40))
256```
257
258See [cmd/colortest/main.go](cmd/colortest/main.go) for a little program to
259display and test colors.
260
261---
262
263For some more advanced cases you can use `Color.String()` directly, but this
264won't look at `zli.WantColor` and you'll need to manually apply the reset code:
265
266```go
267fmt.Println(zli.Red|zli.Bold, "red!")                 // Print escape codes.
268fmt.Println("and bold!", zli.Reset)
269
270fmt.Printf("%sc%so%sl%so%sr%s\n", zli.Red, zli.Magenta, zli.Cyan, zli.Blue, zli.Yellow, zli.Reset)
271```
272
273Because the color is stored in an `uint64` you can assign them to a constant:
274
275```go
276const colorMatch = zli.Bold | zli.Red
277```
278
279This won't work if you use `Color256()` or `ColorHex()`; although you can get
280around this by constructing it all yourself:
281
282```go
283// zli.Color256(99)
284const color = zli.Bold | (zli.Color(99) << zli.ColorOffsetFg) | zli.ColorMode256
285
286// zli.ColorHex("#ff7711").Bg(); can also use 1144831 directly instead of the
287// bit shifts.
288const color2 = zli.Bold | zli.Red | zli.ColorModeTrueBg |
289               (zli.Color(0xff|0x77<<8|0x11<<16) << zli.ColorOffsetBg)
290```
291
292This creates a color stored as an int, shifts it to the correct location, and
293sets the flag to signal how to interpret it.
294
295Do you really want to do this just to create a `const` instead of a `var`?
296Probably not ��
297
298
299### Testing
300
301zli uses to `zli.Stdin`, `zli.Stdout`, `zli.Stderr`, and `zli.Exit` instead of
302the `os.*` variants for everything. You can swap this out with test variants
303with the `zli.Test()` function.
304
305You can use these in your own program as well, if you want to test the output of
306a program.
307
308```go
309func TestX(t *testing.T) {
310    exit, in, out := Test(t) // Resets everything back to os.* with t.Cleanup()
311
312    // Write something to stderr (a bytes.Buffer) and read the output.
313    Error("oh noes!")
314    fmt.Println(out.String()) // zli.test: oh noes!
315
316    // Read from stdin.
317    in.WriteString("Hello")
318    fp, _ := InputOrFile("-", true)
319    got, _ := ioutil.ReadAll(fp)
320    fmt.Println(string(got)) // Hello
321
322    out.Reset()
323
324    et := func() {
325        fmt.Fprintln(Stdout, "one")
326        Exit(1)
327        fmt.Fprintln(Stdout, "two")
328    }
329
330    // exit panics to ensure the regular control flow of the program is aborted;
331    // to capture this run the function to be tested in a closure with
332    // exit.Recover(), which will recover() from the panic and set the exit
333    // code.
334    func() {
335        defer exit.Recover()
336        et()
337    }()
338    // Helper to check the statis code, so you don't have to derefrence and cast
339    // the value to int.
340    exit.Want(t, 1)
341
342    fmt.Println("Exit %d: %s\n", *exit, out.String()) // Exit 1: one
343```
344
345You don't need to use the `zli.Test()` function if you won't want to, you can
346just swap out stuff yourself as well:
347
348```go
349buf := new(bytes.Buffer)
350zli.Stderr = buf
351defer func() { Stderr = os.Stderr }()
352
353Error("oh noes!")
354out := buf.String()
355fmt.Printf("buffer has: %q\n", out) // buffer has: "zli.test: oh noes!\n"
356```
357
358`zli.IsTerminal()` and `zli.TerminalSize()` are variables, and can be swapped
359out as well:
360
361```go
362save := zli.IsTerminal
363zli.IsTerminal = func(uintptr) bool { return true }
364defer func() { IsTerminal = save }()
365```
366
367
368#### Exit
369
370A few notes on replacing `zli.Exit()` in tests: the difficulty with this is that
371`os.Exit()` will terminate the entire program, including the test, which is
372rarely what you want and difficult to test. You can replace `zli.Exit` with
373something like (`zli.TestExit()` takes care of all of this):
374
375```go
376var code int
377zli.Exit = func(c int) { code = c }
378mayExit()
379fmt.Println("exit code", code)
380```
381
382This works well enough for simple cases, but there's a big caveat with this; for
383example consider:
384
385```go
386func mayExit() {
387    err := f()
388    if err != nil {
389        zli.Error(err)
390        zli.Exit(4)
391    }
392
393    fmt.Println("X")
394}
395```
396
397With the above the program will continue after `zli.Exit()`; which is a
398different program flow from normal execution. A simple way to fix it so to
399modify the function to explicitly call `return`:
400
401```go
402func mayExit() {
403    err := f()
404    if err != nil {
405        zli.Error(err)
406        zli.Exit(4)
407        return
408    }
409
410    fmt.Println("X")
411}
412```
413
414This still isn't *quite* the same, as callers of `mayExit()` in your program
415will still continue happily. It's also rather ugly and clunky.
416
417To solve this you can replace `zli.Exit` with a function that panics and then
418recover that:
419
420```go
421func TestFoo(t *testing.T) {
422    var code int
423    zli.Exit = func(c int) {
424        code = c
425        panic("zli.Exit")
426    }
427
428    func() {
429        defer func() {
430            r := recover()
431            if r == nil {
432                return
433            }
434        }()
435
436        mayExit()
437    }()
438
439    fmt.Println("Exited with", code)
440}
441```
442
443This will abort the program flow similar to `os.Exit()`, and the call to
444`mayExit` is wrapped in a function the test function itself will continue after
445the recover.
446