1# ffcli [](https://pkg.go.dev/github.com/peterbourgon/ff/v3/ffcli)
2
3ffcli stands for flags-first command line interface,
4and provides an opinionated way to build CLIs.
5
6## Rationale
7
8Popular CLI frameworks like [spf13/cobra][cobra], [urfave/cli][urfave], or
9[alecthomas/kingpin][kingpin] tend to have extremely large APIs, to support a
10large number of "table stakes" features.
11
12[cobra]: https://github.com/spf13/cobra
13[urfave]: https://github.com/urfave/cli
14[kingpin]: https://github.com/alecthomas/kingpin
15
16This package is intended to be a lightweight alternative to those packages. In
17contrast to them, the API surface area of package ffcli is very small, with the
18immediate goal of being intuitive and productive, and the long-term goal of
19supporting commandline applications that are substantially easier to understand
20and maintain.
21
22To support these goals, the package is concerned only with the core mechanics of
23defining a command tree, parsing flags, and selecting a command to run. It does
24not intend to be a one-stop-shop for everything your commandline application
25needs. Features like tab completion or colorized output are orthogonal to
26command tree parsing, and should be easy to provide on top of ffcli.
27
28Finally, this package follows in the philosophy of its parent package ff, or
29"flags-first". Flags, and more specifically the Go stdlib flag.FlagSet, should
30be the primary mechanism of getting configuration from the execution environment
31into your program. The affordances provided by package ff, including environment
32variable and config file parsing, are also available in package ffcli. Support
33for other flag packages is a non-goal.
34
35
36## Goals
37
38- Absolute minimum usable API
39- Prefer using existing language features/patterns/abstractions whenever possible
40- Enable integration-style testing of CLIs with mockable dependencies
41- No global state
42
43## Non-goals
44
45- All conceivably useful features
46- Integration with flag packages other than [package flag][flag] and [ff][ff]
47
48[flag]: https://golang.org/pkg/flag
49[ff]: https://github.com/peterbourgon/ff
50
51## Usage
52
53The core of the package is the [ffcli.Command][command]. Here is the simplest
54possible example of an ffcli program.
55
56[command]: https://godoc.org/github.com/peterbourgon/ff/ffcli#Command
57
58```go
59import (
60 "context"
61 "os"
62
63 "github.com/peterbourgon/ff/v3/ffcli"
64)
65
66func main() {
67 root := &ffcli.Command{
68 Exec: func(ctx context.Context, args []string) error {
69 println("hello world")
70 return nil
71 },
72 }
73
74 root.ParseAndRun(context.Background(), os.Args[1:])
75}
76```
77
78Most CLIs use flags and arguments to control behavior. Here is a command which
79takes a string to repeat as an argument, and the number of times to repeat it as
80a flag.
81
82```go
83fs := flag.NewFlagSet("repeat", flag.ExitOnError)
84n := fs.Int("n", 3, "how many times to repeat")
85
86root := &ffcli.Command{
87 ShortUsage: "repeat [-n times] <arg>",
88 ShortHelp: "Repeatedly print the argument to stdout.",
89 FlagSet: fs,
90 Exec: func(ctx context.Context, args []string) error {
91 if nargs := len(args); nargs != 1 {
92 return fmt.Errorf("repeat requires exactly 1 argument, but you provided %d", nargs)
93 }
94 for i := 0; i < *n; i++ {
95 fmt.Fprintln(os.Stdout, args[0])
96 }
97 return nil
98 },
99}
100
101if err := root.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
102 log.Fatal(err)
103}
104```
105
106Each command may have subcommands, allowing you to build a command tree.
107
108```go
109var (
110 rootFlagSet = flag.NewFlagSet("textctl", flag.ExitOnError)
111 verbose = rootFlagSet.Bool("v", false, "increase log verbosity")
112 repeatFlagSet = flag.NewFlagSet("textctl repeat", flag.ExitOnError)
113 n = repeatFlagSet.Int("n", 3, "how many times to repeat")
114)
115
116repeat := &ffcli.Command{
117 Name: "repeat",
118 ShortUsage: "textctl repeat [-n times] <arg>",
119 ShortHelp: "Repeatedly print the argument to stdout.",
120 FlagSet: repeatFlagSet,
121 Exec: func(_ context.Context, args []string) error { ... },
122}
123
124count := &ffcli.Command{
125 Name: "count",
126 ShortUsage: "textctl count [<arg> ...]",
127 ShortHelp: "Count the number of bytes in the arguments.",
128 Exec: func(_ context.Context, args []string) error { ... },
129}
130
131root := &ffcli.Command{
132 ShortUsage: "textctl [flags] <subcommand>",
133 FlagSet: rootFlagSet,
134 Subcommands: []*ffcli.Command{repeat, count},
135}
136
137if err := root.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
138 log.Fatal(err)
139}
140```
141
142ParseAndRun can also be split into distinct Parse and Run phases, allowing you
143to perform two-phase setup or initialization of e.g. API clients that require
144user-supplied configuration.
145
146## Examples
147
148See [the examples directory][examples]. If you'd like an example of a specific
149type of program structure, or a CLI that satisfies a specific requirement,
150please [file an issue][issue].
151
152[examples]: https://github.com/peterbourgon/ff/tree/master/ffcli/examples
153[issue]: https://github.com/peterbourgon/ff/issues/new
View as plain text