├── go.mod ├── usage.gotext ├── color.go ├── Makefile ├── License.md ├── args.go ├── strings.go ├── Readme.md ├── Changelog.md ├── stringmap.go ├── string.go ├── arg.go ├── int.go ├── bool.go ├── flag.go ├── enum.go ├── cli.go ├── usage.go ├── go.sum ├── command.go └── cli_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/livebud/cli 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 7 | github.com/matryer/is v1.4.1 8 | github.com/matthewmueller/diff v0.0.3 9 | github.com/matthewmueller/testchild v0.0.1 10 | ) 11 | 12 | require ( 13 | github.com/google/go-cmp v0.6.0 // indirect 14 | github.com/hexops/valast v1.4.4 // indirect 15 | github.com/lithammer/dedent v1.1.0 // indirect 16 | github.com/sergi/go-diff v1.3.1 // indirect 17 | golang.org/x/mod v0.22.0 // indirect 18 | golang.org/x/sync v0.10.0 // indirect 19 | golang.org/x/tools v0.29.0 // indirect 20 | mvdan.cc/gofumpt v0.7.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /usage.gotext: -------------------------------------------------------------------------------- 1 | 2 | {{bold}}Usage:{{reset}} 3 | {{dim}}${{reset}} {{ $.Full }} {{- if $.Usage }}{{ $.Usage }}{{ end }} 4 | 5 | {{- if $.Description}} 6 | 7 | {{bold}}Description:{{reset}} 8 | {{ $.Description }} 9 | {{- end }} 10 | 11 | {{- if $.Flags}} 12 | 13 | {{bold}}Flags:{{reset}} 14 | {{ $.Flags.Usage }} 15 | {{- end }} 16 | 17 | {{- if $.Args }} 18 | 19 | {{bold}}Args:{{reset}} 20 | {{ $.Args.Usage }} 21 | {{- end }} 22 | 23 | {{- if $.Commands }} 24 | 25 | {{bold}}Commands:{{reset}} 26 | {{ $.Commands.Usage }} 27 | {{- end }} 28 | 29 | {{- if $.Advanced }} 30 | 31 | {{bold}}Advanced Commands:{{reset}} 32 | {{ $.Advanced.Usage }} 33 | {{- end }} 34 | 35 | -------------------------------------------------------------------------------- /color.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | ) 7 | 8 | var reset = color("\033[0m") 9 | var dim = color("\033[37m") 10 | 11 | var colors = template.FuncMap{ 12 | "reset": reset, 13 | "bold": color("\033[1m"), 14 | "dim": dim, 15 | "underline": color("\033[4m"), 16 | "teal": color("\033[36m"), 17 | "blue": color("\033[34m"), 18 | "yellow": color("\033[33m"), 19 | "red": color("\033[31m"), 20 | "green": color("\033[32m"), 21 | } 22 | 23 | var nocolor = os.Getenv("NO_COLOR") != "" 24 | 25 | func color(code string) func() string { 26 | return func() string { 27 | if nocolor { 28 | return "" 29 | } 30 | return code 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @ go vet ./... 3 | @ go run honnef.co/go/tools/cmd/staticcheck@latest ./... 4 | @ go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./... 5 | @ go test -race ./... 6 | 7 | precommit: test 8 | 9 | release: VERSION := $(shell awk '/[0-9]+\.[0-9]+\.[0-9]+/ {print $$2; exit}' Changelog.md) 10 | release: test 11 | @ go mod tidy 12 | @ test -n "$(VERSION)" || (echo "Unable to read the version." && false) 13 | @ test -z "`git tag -l v$(VERSION)`" || (echo "Aborting because the v$(VERSION) tag already exists." && false) 14 | @ test -z "`git status --porcelain | grep -vE 'M (Changelog\.md)'`" || (echo "Aborting from uncommitted changes." && false) 15 | @ test -n "`git status --porcelain | grep -v 'M (Changelog\.md)'`" || (echo "Changelog.md must have changes" && false) 16 | @ git commit -am "Release v$(VERSION)" 17 | @ git tag "v$(VERSION)" 18 | @ git push origin main "v$(VERSION)" 19 | @ go run github.com/cli/cli/v2/cmd/gh@latest release create --generate-notes "v$(VERSION)" 20 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matt Mueller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type Args struct { 4 | name string 5 | help string 6 | value value 7 | env *string 8 | } 9 | 10 | func (a *Args) key() string { 11 | return "<" + a.name + "...>" 12 | } 13 | 14 | func (a *Args) verify() error { 15 | return a.value.verify() 16 | } 17 | 18 | // Env allows you to use an environment variable to set the value of the argument. 19 | func (a *Args) Env(name string) *Args { 20 | a.env = &name 21 | return a 22 | } 23 | 24 | func (a *Args) Optional() *OptionalArgs { 25 | return &OptionalArgs{a} 26 | } 27 | 28 | func (a *Args) Strings(target *[]string) *Strings { 29 | *target = []string{} 30 | value := &Strings{target, a.env, nil, false} 31 | a.value = &stringsValue{key: a.key(), inner: value} 32 | return value 33 | } 34 | 35 | func (a *Args) StringMap(target *map[string]string) *StringMap { 36 | *target = map[string]string{} 37 | value := &StringMap{target, a.env, nil, false} 38 | a.value = &stringMapValue{key: "", inner: value} 39 | return value 40 | } 41 | 42 | type OptionalArgs struct { 43 | a *Args 44 | } 45 | 46 | func (a *OptionalArgs) key() string { 47 | return "[<" + a.a.name + ">...]" 48 | } 49 | 50 | func (a *OptionalArgs) Strings(target *[]string) *Strings { 51 | value := &Strings{target, a.a.env, nil, true} 52 | a.a.value = &stringsValue{key: a.key(), inner: value} 53 | return value 54 | } 55 | 56 | func (a *OptionalArgs) StringMap(target *map[string]string) *StringMap { 57 | value := &StringMap{target, a.a.env, nil, true} 58 | a.a.value = &stringMapValue{key: a.key(), inner: value} 59 | return value 60 | } 61 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kballard/go-shellquote" 8 | ) 9 | 10 | type Strings struct { 11 | target *[]string 12 | envvar *string 13 | defval *[]string // default value 14 | optional bool 15 | } 16 | 17 | func (v *Strings) Default(values ...string) { 18 | v.defval = &values 19 | } 20 | 21 | type stringsValue struct { 22 | key string 23 | inner *Strings 24 | set bool 25 | } 26 | 27 | var _ value = (*stringsValue)(nil) 28 | 29 | func (v *stringsValue) optional() bool { 30 | return v.inner.optional 31 | } 32 | 33 | func (v *stringsValue) verify() error { 34 | if v.set { 35 | return nil 36 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 37 | fields, err := shellquote.Split(value) 38 | if err != nil { 39 | return fmt.Errorf("%s: expected a list of strings but got %q", v.key, value) 40 | } 41 | for _, kv := range fields { 42 | if err := v.Set(kv); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } else if v.hasDefault() { 48 | *v.inner.target = *v.inner.defval 49 | return nil 50 | } else if v.inner.optional { 51 | return nil 52 | } 53 | return &missingInputError{v.key, v.inner.envvar} 54 | } 55 | 56 | func (v *stringsValue) hasDefault() bool { 57 | return v.inner.defval != nil 58 | } 59 | 60 | func (v *stringsValue) Default() (string, bool) { 61 | if v.inner.defval == nil { 62 | return "", false 63 | } 64 | def := strings.Join(*v.inner.defval, ", ") 65 | if def == "" { 66 | return "[]", true 67 | } 68 | return def, true 69 | } 70 | 71 | func (v *stringsValue) Set(val string) error { 72 | *v.inner.target = append(*v.inner.target, val) 73 | v.set = true 74 | return nil 75 | } 76 | 77 | func (v *stringsValue) String() string { 78 | if v.inner == nil { 79 | return "" 80 | } else if v.set { 81 | return strings.Join(*v.inner.target, ", ") 82 | } else if v.hasDefault() { 83 | return strings.Join(*v.inner.defval, ", ") 84 | } 85 | return "" 86 | } 87 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/livebud/cli.svg)](https://pkg.go.dev/github.com/livebud/cli) 4 | 5 | Build beautiful CLIs in Go. A simpler alternative to [kingpin](https://github.com/alecthomas/kingpin). 6 | 7 | CleanShot 2023-06-20 at 22 23 14@2x 8 | 9 | ## Features 10 | 11 | - Type-safe, fluent API 12 | - Flag, command and argument support 13 | - Required and optional parameters 14 | - Built entirely on the [flag](https://pkg.go.dev/flag) package the standard library 15 | - Supports both space-based and colon-based subcommands (e.g. `controller new` & `controller:new`) 16 | - `SIGINT` context cancellation out-of-the-box 17 | - Custom help messages 18 | - Built-in tab completion with `complete -o nospace -C ` 19 | - Respects `NO_COLOR` 20 | 21 | ## Install 22 | 23 | ```sh 24 | go get -u github.com/livebud/cli 25 | ``` 26 | 27 | ## Example 28 | 29 | ```go 30 | package main 31 | 32 | import ( 33 | "context" 34 | "fmt" 35 | "os" 36 | 37 | "github.com/livebud/cli" 38 | ) 39 | 40 | func main() { 41 | flag := new(Flag) 42 | cli := cli.New("app", "your awesome cli").Writer(os.Stdout) 43 | cli.Flag("log", "log level").Short('L').String(&flag.Log).Default("info") 44 | cli.Flag("embed", "embed the code").Bool(&flag.Embed).Default(false) 45 | 46 | { // new 47 | cmd := &New{Flag: flag} 48 | cli := cli.Command("new", "create a new project") 49 | cli.Arg("dir").String(&cmd.Dir) 50 | cli.Run(cmd.Run) 51 | } 52 | 53 | ctx := context.Background() 54 | if err := cli.Parse(ctx, os.Args[1:]...); err != nil { 55 | fmt.Fprintln(os.Stderr, err) 56 | os.Exit(1) 57 | } 58 | } 59 | 60 | type Flag struct { 61 | Log string 62 | Embed bool 63 | } 64 | 65 | type New struct { 66 | Flag *Flag 67 | Dir string 68 | } 69 | 70 | // Run new 71 | func (n *New) Run(ctx context.Context) error { 72 | return nil 73 | } 74 | ``` 75 | 76 | ## Contributing 77 | 78 | First, clone the repo: 79 | 80 | ```sh 81 | git clone https://github.com/livebud/cli 82 | cd cli 83 | ``` 84 | 85 | Next, install dependencies: 86 | 87 | ```sh 88 | go mod tidy 89 | ``` 90 | 91 | Finally, try running the tests: 92 | 93 | ```sh 94 | go test ./... 95 | ``` 96 | 97 | ## License 98 | 99 | MIT 100 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # 0.0.22 / 2025-11-10 2 | 3 | - fix handling argument -, e.g. `$ base58 -` 4 | 5 | # 0.0.21 / 2025-09-17 6 | 7 | - remove unused version 8 | - when calling the CLI with go run or go test, don't trap os.Interrupt by default to avoid "double Ctrl-C" problem 9 | - add support for command middleware 10 | 11 | # 0.0.20 / 2025-09-02 12 | 13 | - update test to verify env error 14 | - add `$` to missing input error 15 | - handle `$` prefix in `.Env($ENV_VAR)` 16 | 17 | # 0.0.19 / 2025-09-02 18 | 19 | - bump up go and run modernize 20 | 21 | # 0.0.18 / 2025-09-02 22 | 23 | - don't panic with missing value setter. minor usage improvement 24 | 25 | # 0.0.17 / 2025-04-19 26 | 27 | - fix -- for subcommands 28 | 29 | # 0.0.16 / 2025-04-19 30 | 31 | - fix so `--` appends an unparsed arg 32 | 33 | # 0.0.15 / 2025-04-19 34 | 35 | - ignore args after `--` 36 | 37 | # 0.0.14 / 2025-01-20 38 | 39 | - better empty default values for string arrays and maps 40 | 41 | # 0.0.13 / 2025-01-20 42 | 43 | - add support for using environment variables 44 | - usage now shows defaults and optionals 45 | - better error messages for args. 46 | 47 | # 0.0.12 / 2025-01-12 48 | 49 | - fix bug where flags were sometimes getting shared on subcommands 50 | 51 | # 0.0.11 / 2024-12-07 52 | 53 | - support passing flags out of order 54 | 55 | # 0.0.10 / 2024-10-26 56 | 57 | - fix help message coloring 58 | 59 | # 0.0.9 / 2024-10-26 60 | 61 | - support basic tab completion using `COMP_LINE` and `complete -o nospace -C ` 62 | 63 | # 0.0.8 / 2024-10-26 64 | 65 | - **BREAKING:** switch away from colon-based subcommands. 66 | You can still use colon-based commands, you just use them as commands like `cli.Command("fs:cat", ...)` and don't nest. 67 | - add ability to find and modify commands 68 | 69 | # 0.0.7 / 2024-05-04 70 | 71 | - better error message for enums 72 | 73 | # 0.0.6 / 2024-05-04 74 | 75 | - add help message back in for args (not used yet) 76 | 77 | # 0.0.5 / 2024-05-04 78 | 79 | - add enum support 80 | - Remove unused add ability to infer flags and args from struct tags 81 | - Remove unused support calling multiple runners at once 82 | 83 | # 0.0.4 / 2023-10-22 84 | 85 | - update release script 86 | 87 | # 0.0.3 / 2023-10-22 88 | 89 | - Add ability to infer flags and args from struct tags. 90 | - Support calling multiple runners at once 91 | 92 | # 0.0.2 / 2023-10-07 93 | 94 | - switch to using signal.NotifyContext 95 | 96 | # 0.0.1 / 2023-06-21 97 | 98 | - Initial release 99 | -------------------------------------------------------------------------------- /stringmap.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kballard/go-shellquote" 8 | ) 9 | 10 | type StringMap struct { 11 | target *map[string]string 12 | envvar *string 13 | defval *map[string]string // default value 14 | optional bool 15 | } 16 | 17 | func (v *StringMap) Default(value map[string]string) { 18 | v.defval = &value 19 | } 20 | 21 | type stringMapValue struct { 22 | key string 23 | inner *StringMap 24 | set bool 25 | } 26 | 27 | var _ value = (*stringMapValue)(nil) 28 | 29 | func (v *stringMapValue) optional() bool { 30 | return false 31 | } 32 | 33 | func (v *stringMapValue) verify() error { 34 | if v.set { 35 | return nil 36 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 37 | fields, err := shellquote.Split(value) 38 | if err != nil { 39 | return fmt.Errorf("%s: expected a string map but got %q", v.key, value) 40 | } 41 | for _, kv := range fields { 42 | if err := v.Set(kv); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } else if v.hasDefault() { 48 | *v.inner.target = *v.inner.defval 49 | return nil 50 | } else if v.inner.optional { 51 | return nil 52 | } 53 | return &missingInputError{v.key, v.inner.envvar} 54 | } 55 | 56 | func (v *stringMapValue) hasDefault() bool { 57 | return v.inner.defval != nil 58 | } 59 | 60 | func (v *stringMapValue) Default() (string, bool) { 61 | if v.inner.defval == nil { 62 | return "", false 63 | } 64 | str := new(strings.Builder) 65 | i := 0 66 | for k, v := range *v.inner.defval { 67 | if i > 0 { 68 | str.WriteString(" ") 69 | } 70 | str.WriteString(k + ":" + v) 71 | i++ 72 | } 73 | if str.Len() == 0 { 74 | return "{}", true 75 | } 76 | return str.String(), true 77 | } 78 | 79 | func (v *stringMapValue) Set(val string) error { 80 | kv := strings.SplitN(val, ":", 2) 81 | if len(kv) != 2 { 82 | return fmt.Errorf("%s: invalid key:value pair for %q", v.key, val) 83 | } 84 | if *v.inner.target == nil { 85 | *v.inner.target = map[string]string{} 86 | } 87 | (*v.inner.target)[kv[0]] = kv[1] 88 | v.set = true 89 | return nil 90 | } 91 | 92 | func (v *stringMapValue) String() string { 93 | if v.inner == nil { 94 | return "" 95 | } else if v.set { 96 | return v.format(*v.inner.target) 97 | } else if v.hasDefault() { 98 | return v.format(*v.inner.defval) 99 | } 100 | return "" 101 | } 102 | 103 | // Format as a string 104 | func (v *stringMapValue) format(kv map[string]string) (out string) { 105 | i := 0 106 | for k, v := range kv { 107 | if i > 0 { 108 | out += " " 109 | } 110 | out += k + ":" + v 111 | i++ 112 | } 113 | return out 114 | } 115 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type String struct { 4 | target *string 5 | envvar *string 6 | defval *string // default value 7 | } 8 | 9 | func (v *String) Default(value string) { 10 | v.defval = &value 11 | } 12 | 13 | type stringValue struct { 14 | key string 15 | inner *String 16 | set bool 17 | } 18 | 19 | func (v *stringValue) optional() bool { 20 | return false 21 | } 22 | 23 | var _ value = (*stringValue)(nil) 24 | 25 | func (v *stringValue) verify() error { 26 | if v.set { 27 | return nil 28 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 29 | return v.Set(value) 30 | } else if v.hasDefault() { 31 | *v.inner.target = *v.inner.defval 32 | return nil 33 | } 34 | return &missingInputError{v.key, v.inner.envvar} 35 | } 36 | 37 | func (v *stringValue) hasDefault() bool { 38 | return v.inner.defval != nil 39 | } 40 | 41 | func (v *stringValue) Default() (string, bool) { 42 | if v.inner.defval == nil { 43 | return "", false 44 | } 45 | return *v.inner.defval, true 46 | } 47 | 48 | func (v *stringValue) Set(val string) error { 49 | *v.inner.target = val 50 | v.set = true 51 | return nil 52 | } 53 | 54 | func (v *stringValue) String() string { 55 | if v.inner == nil { 56 | return "" 57 | } else if v.set { 58 | return *v.inner.target 59 | } else if v.hasDefault() { 60 | return *v.inner.defval 61 | } 62 | return "" 63 | } 64 | 65 | type OptionalString struct { 66 | target **string 67 | envvar *string 68 | defval *string // default value 69 | } 70 | 71 | func (v *OptionalString) Default(value string) { 72 | v.defval = &value 73 | } 74 | 75 | type optionalStringValue struct { 76 | key string 77 | inner *OptionalString 78 | set bool 79 | } 80 | 81 | var _ value = (*optionalStringValue)(nil) 82 | 83 | func (v *optionalStringValue) optional() bool { 84 | return true 85 | } 86 | 87 | func (v *optionalStringValue) hasDefault() bool { 88 | return v.inner.defval != nil 89 | } 90 | 91 | func (v *optionalStringValue) Default() (string, bool) { 92 | if v.inner.defval == nil { 93 | return "", false 94 | } 95 | return *v.inner.defval, true 96 | } 97 | 98 | func (v *optionalStringValue) verify() error { 99 | if v.set { 100 | return nil 101 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 102 | return v.Set(value) 103 | } else if v.hasDefault() { 104 | *v.inner.target = v.inner.defval 105 | return nil 106 | } 107 | return nil 108 | } 109 | 110 | func (v *optionalStringValue) Set(val string) error { 111 | *v.inner.target = &val 112 | v.set = true 113 | return nil 114 | } 115 | 116 | func (v *optionalStringValue) String() string { 117 | if v.inner == nil { 118 | return "" 119 | } else if v.set { 120 | return **v.inner.target 121 | } else if v.hasDefault() { 122 | return *v.inner.defval 123 | } 124 | return "" 125 | } 126 | -------------------------------------------------------------------------------- /arg.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type Arg struct { 4 | name string 5 | help string 6 | value value 7 | env *string 8 | } 9 | 10 | func (a *Arg) key() string { 11 | return "<" + a.name + ">" 12 | } 13 | 14 | // Env allows you to use an environment variable to set the value of the argument. 15 | func (a *Arg) Env(name string) *Arg { 16 | a.env = &name 17 | return a 18 | } 19 | 20 | func (a *Arg) Optional() *OptionalArg { 21 | return &OptionalArg{a} 22 | } 23 | 24 | func (a *Arg) Int(target *int) *Int { 25 | value := &Int{target, a.env, nil} 26 | a.value = &intValue{key: a.key(), inner: value} 27 | return value 28 | } 29 | 30 | func (a *Arg) Bool(target *bool) *Bool { 31 | value := &Bool{target, a.env, nil} 32 | a.value = &boolValue{key: a.key(), inner: value} 33 | return value 34 | } 35 | 36 | func (a *Arg) String(target *string) *String { 37 | value := &String{target, a.env, nil} 38 | a.value = &stringValue{key: a.key(), inner: value} 39 | return value 40 | } 41 | 42 | func (a *Arg) Enum(target *string, possibilities ...string) *Enum { 43 | value := &Enum{target, a.env, nil} 44 | a.value = &enumValue{key: a.key(), inner: value, possibilities: possibilities} 45 | return value 46 | } 47 | 48 | // StringMap accepts a key-value pair in the form of "". 49 | func (a *Arg) StringMap(target *map[string]string) *StringMap { 50 | *target = map[string]string{} 51 | value := &StringMap{target, a.env, nil, false} 52 | a.value = &stringMapValue{key: "", inner: value} 53 | return value 54 | } 55 | 56 | func (a *Arg) verify() error { 57 | return a.value.verify() 58 | } 59 | 60 | type OptionalArg struct { 61 | a *Arg 62 | } 63 | 64 | func (a *OptionalArg) key() string { 65 | return "<" + a.a.name + ">" 66 | } 67 | 68 | func (a *OptionalArg) String(target **string) *OptionalString { 69 | value := &OptionalString{target, a.a.env, nil} 70 | a.a.value = &optionalStringValue{key: a.key(), inner: value} 71 | return value 72 | } 73 | 74 | func (a *OptionalArg) Int(target **int) *OptionalInt { 75 | value := &OptionalInt{target, a.a.env, nil} 76 | a.a.value = &optionalIntValue{key: a.key(), inner: value} 77 | return value 78 | } 79 | 80 | func (a *OptionalArg) Bool(target **bool) *OptionalBool { 81 | value := &OptionalBool{target, a.a.env, nil} 82 | a.a.value = &optionalBoolValue{key: a.key(), inner: value} 83 | return value 84 | } 85 | 86 | func (a *OptionalArg) Enum(target **string, possibilities ...string) *OptionalEnum { 87 | value := &OptionalEnum{target, a.a.env, nil} 88 | a.a.value = &optionalEnumValue{key: a.key(), inner: value, possibilities: possibilities} 89 | return value 90 | } 91 | 92 | func (a *OptionalArg) StringMap(target *map[string]string) *StringMap { 93 | *target = map[string]string{} 94 | value := &StringMap{target, a.a.env, nil, true} 95 | a.a.value = &stringMapValue{key: a.key(), inner: value} 96 | return value 97 | } 98 | 99 | func verifyArgs(args []*Arg) error { 100 | for _, arg := range args { 101 | if err := arg.verify(); err != nil { 102 | return err 103 | } 104 | } 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /int.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type Int struct { 9 | target *int 10 | envvar *string 11 | defval *int 12 | } 13 | 14 | func (v *Int) Default(value int) { 15 | v.defval = &value 16 | } 17 | 18 | type intValue struct { 19 | key string 20 | inner *Int 21 | set bool 22 | } 23 | 24 | var _ value = (*intValue)(nil) 25 | 26 | func (v *intValue) optional() bool { 27 | return false 28 | } 29 | 30 | func (v *intValue) hasDefault() bool { 31 | return v.inner.defval != nil 32 | } 33 | 34 | func (v *intValue) Default() (string, bool) { 35 | if v.inner.defval == nil { 36 | return "", false 37 | } 38 | return strconv.Itoa(*v.inner.defval), true 39 | } 40 | 41 | func (v *intValue) verify() error { 42 | if v.set { 43 | return nil 44 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 45 | return v.Set(value) 46 | } else if v.hasDefault() { 47 | *v.inner.target = *v.inner.defval 48 | return nil 49 | } 50 | return &missingInputError{v.key, v.inner.envvar} 51 | } 52 | 53 | func (v *intValue) Set(val string) error { 54 | n, err := strconv.Atoi(val) 55 | if err != nil { 56 | return fmt.Errorf("%s: expected an integer but got %q", v.key, val) 57 | } 58 | *v.inner.target = n 59 | v.set = true 60 | return nil 61 | } 62 | 63 | func (v *intValue) String() string { 64 | if v.inner == nil { 65 | return "" 66 | } else if v.set { 67 | return strconv.Itoa(*v.inner.target) 68 | } else if v.hasDefault() { 69 | return strconv.Itoa(*v.inner.defval) 70 | } 71 | return "" 72 | } 73 | 74 | type OptionalInt struct { 75 | target **int 76 | envvar *string 77 | defval *int 78 | } 79 | 80 | func (v *OptionalInt) Default(value int) { 81 | v.defval = &value 82 | } 83 | 84 | type optionalIntValue struct { 85 | key string 86 | inner *OptionalInt 87 | set bool 88 | } 89 | 90 | var _ value = (*optionalIntValue)(nil) 91 | 92 | func (v *optionalIntValue) optional() bool { 93 | return true 94 | } 95 | 96 | func (v *optionalIntValue) hasDefault() bool { 97 | return v.inner.defval != nil 98 | } 99 | 100 | func (v *optionalIntValue) Default() (string, bool) { 101 | if v.inner.defval == nil { 102 | return "", false 103 | } 104 | return strconv.Itoa(*v.inner.defval), true 105 | } 106 | 107 | func (v *optionalIntValue) verify() error { 108 | if v.set { 109 | return nil 110 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 111 | return v.Set(value) 112 | } else if v.hasDefault() { 113 | *v.inner.target = v.inner.defval 114 | return nil 115 | } 116 | return nil 117 | } 118 | 119 | func (v *optionalIntValue) Set(val string) error { 120 | n, err := strconv.Atoi(val) 121 | if err != nil { 122 | return fmt.Errorf("%s: expected an integer but got %q", v.key, val) 123 | } 124 | *v.inner.target = &n 125 | v.set = true 126 | return nil 127 | } 128 | 129 | func (v *optionalIntValue) String() string { 130 | if v.inner == nil { 131 | return "" 132 | } else if v.set { 133 | return strconv.Itoa(**v.inner.target) 134 | } else if v.hasDefault() { 135 | return strconv.Itoa(*v.inner.defval) 136 | } 137 | return "" 138 | } 139 | -------------------------------------------------------------------------------- /bool.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type Bool struct { 9 | target *bool 10 | envvar *string 11 | defval *bool // default value 12 | } 13 | 14 | func (v *Bool) Default(value bool) { 15 | v.defval = &value 16 | } 17 | 18 | type boolValue struct { 19 | key string 20 | inner *Bool 21 | set bool 22 | } 23 | 24 | var _ value = (*boolValue)(nil) 25 | 26 | func (v *boolValue) optional() bool { 27 | return false 28 | } 29 | 30 | func (v *boolValue) hasDefault() bool { 31 | return v.inner.defval != nil 32 | } 33 | 34 | func (v *boolValue) Default() (string, bool) { 35 | if v.inner.defval == nil { 36 | return "", false 37 | } 38 | return strconv.FormatBool(*v.inner.defval), true 39 | } 40 | 41 | func (v *boolValue) verify() error { 42 | if v.set { 43 | return nil 44 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 45 | return v.Set(value) 46 | } else if v.hasDefault() { 47 | *v.inner.target = *v.inner.defval 48 | return nil 49 | } 50 | return &missingInputError{v.key, v.inner.envvar} 51 | } 52 | 53 | func (v *boolValue) Set(val string) (err error) { 54 | *v.inner.target, err = strconv.ParseBool(val) 55 | if err != nil { 56 | return fmt.Errorf("%s: expected a boolean but got %q", v.key, val) 57 | } 58 | v.set = true 59 | return nil 60 | } 61 | 62 | func (v *boolValue) String() string { 63 | if v.inner == nil { 64 | return "" 65 | } else if v.set { 66 | return strconv.FormatBool(*v.inner.target) 67 | } else if v.hasDefault() { 68 | return strconv.FormatBool(*v.inner.defval) 69 | } 70 | return "false" 71 | } 72 | 73 | // IsBoolFlag allows --flag to be an alias for --flag true 74 | func (v *boolValue) IsBoolFlag() bool { 75 | return true 76 | } 77 | 78 | type OptionalBool struct { 79 | target **bool 80 | envvar *string 81 | defval *bool // default value 82 | } 83 | 84 | func (v *OptionalBool) Default(value bool) { 85 | v.defval = &value 86 | } 87 | 88 | type optionalBoolValue struct { 89 | key string 90 | inner *OptionalBool 91 | set bool 92 | } 93 | 94 | var _ value = (*optionalBoolValue)(nil) 95 | 96 | // IsBoolFlag allows --flag to be an alias for --flag true 97 | func (v *optionalBoolValue) IsBoolFlag() bool { 98 | return true 99 | } 100 | 101 | func (v *optionalBoolValue) optional() bool { 102 | return true 103 | } 104 | 105 | func (v *optionalBoolValue) Default() (string, bool) { 106 | if v.inner.defval == nil { 107 | return "", false 108 | } 109 | return strconv.FormatBool(*v.inner.defval), true 110 | } 111 | 112 | func (v *optionalBoolValue) hasDefault() bool { 113 | return v.inner.defval != nil 114 | } 115 | 116 | func (v *optionalBoolValue) verify() error { 117 | if v.set { 118 | return nil 119 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 120 | return v.Set(value) 121 | } else if v.hasDefault() { 122 | *v.inner.target = v.inner.defval 123 | return nil 124 | } 125 | return nil 126 | } 127 | 128 | func (v *optionalBoolValue) Set(val string) error { 129 | b, err := strconv.ParseBool(val) 130 | if err != nil { 131 | return fmt.Errorf("%s: expected a boolean but got %q", v.key, val) 132 | } 133 | *v.inner.target = &b 134 | v.set = true 135 | return nil 136 | } 137 | 138 | func (v *optionalBoolValue) String() string { 139 | if v.inner == nil { 140 | return "" 141 | } else if v.set { 142 | return strconv.FormatBool(**v.inner.target) 143 | } else if v.hasDefault() { 144 | return strconv.FormatBool(*v.inner.defval) 145 | } 146 | return "" 147 | } 148 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "strings" 4 | 5 | type Flag struct { 6 | name string 7 | help string 8 | short string 9 | env *string 10 | value value 11 | } 12 | 13 | func (f *Flag) key() string { 14 | return "--" + f.name 15 | } 16 | 17 | // Short allows you to specify a short name for the flag. 18 | func (f *Flag) Short(short byte) *Flag { 19 | f.short = string(short) 20 | return f 21 | } 22 | 23 | // Env allows you to use an environment variable to set the value of the flag. 24 | func (f *Flag) Env(name string) *Flag { 25 | name = strings.TrimPrefix(name, "$") 26 | f.env = &name 27 | return f 28 | } 29 | 30 | func (f *Flag) Optional() *OptionalFlag { 31 | return &OptionalFlag{f} 32 | } 33 | 34 | func (f *Flag) Int(target *int) *Int { 35 | value := &Int{target, f.env, nil} 36 | f.value = &intValue{key: f.key(), inner: value} 37 | return value 38 | } 39 | 40 | func (f *Flag) String(target *string) *String { 41 | value := &String{target, f.env, nil} 42 | f.value = &stringValue{key: f.key(), inner: value} 43 | return value 44 | } 45 | 46 | func (f *Flag) Strings(target *[]string) *Strings { 47 | *target = []string{} 48 | value := &Strings{target, f.env, nil, false} 49 | f.value = &stringsValue{key: f.key(), inner: value} 50 | return value 51 | } 52 | 53 | func (f *Flag) Enum(target *string, possibilities ...string) *Enum { 54 | value := &Enum{target, f.env, nil} 55 | f.value = &enumValue{key: f.key(), inner: value, possibilities: possibilities} 56 | return value 57 | } 58 | 59 | func (f *Flag) StringMap(target *map[string]string) *StringMap { 60 | *target = map[string]string{} 61 | value := &StringMap{target, f.env, nil, false} 62 | f.value = &stringMapValue{key: f.key(), inner: value} 63 | return value 64 | } 65 | 66 | func (f *Flag) Bool(target *bool) *Bool { 67 | value := &Bool{target, f.env, nil} 68 | f.value = &boolValue{key: f.key(), inner: value} 69 | return value 70 | } 71 | 72 | func (f *Flag) verify(name string) error { 73 | return f.value.verify() 74 | } 75 | 76 | type OptionalFlag struct { 77 | f *Flag 78 | } 79 | 80 | func (f *OptionalFlag) key() string { 81 | return "--" + f.f.name 82 | } 83 | 84 | func (f *OptionalFlag) String(target **string) *OptionalString { 85 | value := &OptionalString{target, f.f.env, nil} 86 | f.f.value = &optionalStringValue{key: f.key(), inner: value} 87 | return value 88 | } 89 | 90 | func (f *OptionalFlag) Int(target **int) *OptionalInt { 91 | value := &OptionalInt{target, f.f.env, nil} 92 | f.f.value = &optionalIntValue{key: f.key(), inner: value} 93 | return value 94 | } 95 | 96 | func (f *OptionalFlag) Bool(target **bool) *OptionalBool { 97 | value := &OptionalBool{target, f.f.env, nil} 98 | f.f.value = &optionalBoolValue{key: f.key(), inner: value} 99 | return value 100 | } 101 | 102 | func (f *OptionalFlag) Strings(target *[]string) *Strings { 103 | value := &Strings{target, f.f.env, nil, true} 104 | f.f.value = &stringsValue{key: f.key(), inner: value} 105 | return value 106 | } 107 | 108 | func (f *OptionalFlag) StringMap(target *map[string]string) *StringMap { 109 | value := &StringMap{target, f.f.env, nil, true} 110 | f.f.value = &stringMapValue{key: f.key(), inner: value} 111 | return value 112 | } 113 | 114 | func (f *OptionalFlag) Enum(target **string, possibilities ...string) *OptionalEnum { 115 | value := &OptionalEnum{target, f.f.env, nil} 116 | f.f.value = &optionalEnumValue{key: f.key(), inner: value, possibilities: possibilities} 117 | return value 118 | } 119 | 120 | func verifyFlags(flags []*Flag) error { 121 | for _, flag := range flags { 122 | if err := flag.verify(flag.name); err != nil { 123 | return err 124 | } 125 | } 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /enum.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type Enum struct { 11 | target *string 12 | envvar *string 13 | defval *string // default value 14 | } 15 | 16 | func (v *Enum) Default(value string) { 17 | v.defval = &value 18 | } 19 | 20 | func (v *Enum) Env(name string) { 21 | v.envvar = &name 22 | } 23 | 24 | type enumValue struct { 25 | key string 26 | inner *Enum 27 | set bool 28 | possibilities []string 29 | } 30 | 31 | var _ value = (*enumValue)(nil) 32 | 33 | func (v *enumValue) optional() bool { 34 | return false 35 | } 36 | 37 | func (v *enumValue) hasDefault() bool { 38 | return v.inner.defval != nil 39 | } 40 | 41 | func (v *enumValue) Default() (string, bool) { 42 | if v.inner.defval == nil { 43 | return "", false 44 | } 45 | return *v.inner.defval, true 46 | } 47 | 48 | func (v *enumValue) verify() error { 49 | if v.set { 50 | return nil 51 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 52 | return v.Set(value) 53 | } else if v.hasDefault() { 54 | if err := verifyEnum(v.key, *v.inner.defval, v.possibilities...); err != nil { 55 | return err 56 | } 57 | *v.inner.target = *v.inner.defval 58 | return nil 59 | } 60 | return &missingInputError{v.key, v.inner.envvar} 61 | } 62 | 63 | func (v *enumValue) Set(val string) error { 64 | if err := verifyEnum(v.key, val, v.possibilities...); err != nil { 65 | return err 66 | } 67 | *v.inner.target = val 68 | v.set = true 69 | return nil 70 | } 71 | 72 | func (v *enumValue) String() string { 73 | if v.inner == nil { 74 | return "" 75 | } else if v.set { 76 | return *v.inner.target 77 | } else if v.hasDefault() { 78 | return *v.inner.defval 79 | } 80 | return "" 81 | } 82 | 83 | type OptionalEnum struct { 84 | target **string 85 | envvar *string 86 | defval *string // default value 87 | } 88 | 89 | func (v *OptionalEnum) Default(value string) { 90 | v.defval = &value 91 | } 92 | 93 | func (v *OptionalEnum) Env(name string) { 94 | v.envvar = &name 95 | } 96 | 97 | type optionalEnumValue struct { 98 | key string 99 | inner *OptionalEnum 100 | set bool 101 | possibilities []string 102 | } 103 | 104 | var _ value = (*optionalEnumValue)(nil) 105 | 106 | func (v *optionalEnumValue) optional() bool { 107 | return true 108 | } 109 | 110 | func (v *optionalEnumValue) hasDefault() bool { 111 | return v.inner.defval != nil 112 | } 113 | 114 | func (v *optionalEnumValue) Default() (string, bool) { 115 | if v.inner.defval == nil { 116 | return "", false 117 | } 118 | return *v.inner.defval, true 119 | } 120 | 121 | func (v *optionalEnumValue) verify() error { 122 | if v.set { 123 | return nil 124 | } else if value, ok := lookupEnv(v.inner.envvar); ok { 125 | return v.Set(value) 126 | } else if v.hasDefault() { 127 | if err := verifyEnum(v.key, *v.inner.defval, v.possibilities...); err != nil { 128 | return err 129 | } 130 | *v.inner.target = v.inner.defval 131 | return nil 132 | } 133 | return nil 134 | } 135 | 136 | func (v *optionalEnumValue) Set(val string) error { 137 | if err := verifyEnum(v.key, val, v.possibilities...); err != nil { 138 | return err 139 | } 140 | *v.inner.target = &val 141 | v.set = true 142 | return nil 143 | } 144 | 145 | func (v *optionalEnumValue) String() string { 146 | if v.inner == nil { 147 | return "" 148 | } else if v.set { 149 | return **v.inner.target 150 | } else if v.hasDefault() { 151 | return *v.inner.defval 152 | } 153 | return "" 154 | } 155 | 156 | func verifyEnum(key, val string, possibilities ...string) error { 157 | if slices.Contains(possibilities, val) { 158 | return nil 159 | } 160 | s := new(strings.Builder) 161 | lp := len(possibilities) 162 | for i, p := range possibilities { 163 | if i == lp-1 { 164 | s.WriteString(" or ") 165 | } else if i > 0 { 166 | s.WriteString(", ") 167 | } 168 | s.WriteString(strconv.Quote(p)) 169 | } 170 | return fmt.Errorf("%s %q must be either %s", key, val, s.String()) 171 | } 172 | -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/signal" 11 | "path/filepath" 12 | "strings" 13 | "text/template" 14 | ) 15 | 16 | var ErrInvalidInput = errors.New("cli: invalid input") 17 | var ErrCommandNotFound = errors.New("cli: command not found") 18 | 19 | type Middleware = func(next func(ctx context.Context) error) func(ctx context.Context) error 20 | 21 | type Command interface { 22 | Command(name, help string) Command 23 | Hidden() Command 24 | Advanced() Command 25 | Flag(name, help string) *Flag 26 | Arg(name, help string) *Arg 27 | Args(name, help string) *Args 28 | Use(middlewares ...Middleware) Command 29 | Run(runner func(ctx context.Context) error) 30 | } 31 | 32 | func New(name, help string) *CLI { 33 | config := &config{os.Stdout, defaultUsage, defaultSignals()} 34 | return &CLI{newCommand(config, []*Flag{}, name, name, help), config} 35 | } 36 | 37 | type CLI struct { 38 | root *command 39 | config *config 40 | } 41 | 42 | var _ Command = (*CLI)(nil) 43 | 44 | type config struct { 45 | writer io.Writer 46 | usage *template.Template 47 | signals []os.Signal 48 | } 49 | 50 | func (c *CLI) Writer(writer io.Writer) *CLI { 51 | c.config.writer = writer 52 | return c 53 | } 54 | 55 | func (c *CLI) Usage(usage *template.Template) *CLI { 56 | c.config.usage = usage 57 | return c 58 | } 59 | 60 | func (c *CLI) Trap(signals ...os.Signal) *CLI { 61 | c.config.signals = signals 62 | return c 63 | } 64 | 65 | func (c *CLI) Parse(ctx context.Context, args ...string) error { 66 | // Trap signals if any were provided 67 | ctx = trap(ctx, c.config.signals...) 68 | // Support basic tab completion 69 | if compline := os.Getenv("COMP_LINE"); compline != "" { 70 | return c.complete(compline) 71 | } 72 | // Parse the command line arguments 73 | if err := c.root.parse(ctx, args); err != nil { 74 | return err 75 | } 76 | // Give the caller a chance to handle context cancellations and therefore 77 | // interrupts specifically. 78 | return ctx.Err() 79 | } 80 | 81 | func (c *CLI) Command(name, help string) Command { 82 | return c.root.Command(name, help) 83 | } 84 | 85 | func (c *CLI) Hidden() Command { 86 | return c.root.Hidden() 87 | } 88 | 89 | func (c *CLI) Advanced() Command { 90 | return c.root.Advanced() 91 | } 92 | 93 | func (c *CLI) Flag(name, help string) *Flag { 94 | return c.root.Flag(name, help) 95 | } 96 | 97 | func (c *CLI) Arg(name, help string) *Arg { 98 | return c.root.Arg(name, help) 99 | } 100 | 101 | func (c *CLI) Args(name, help string) *Args { 102 | return c.root.Args(name, help) 103 | } 104 | 105 | func (c *CLI) Run(runner func(ctx context.Context) error) { 106 | c.root.Run(runner) 107 | } 108 | 109 | func (c *CLI) Use(middlewares ...Middleware) Command { 110 | return c.root.Use(middlewares...) 111 | } 112 | 113 | func (c *CLI) Find(subcommand ...string) (Command, error) { 114 | return c.find(subcommand...) 115 | } 116 | 117 | func (c *CLI) find(subcommand ...string) (*command, error) { 118 | sub, ok := c.root.Find(subcommand...) 119 | if !ok { 120 | return nil, fmt.Errorf("%w: %s", ErrCommandNotFound, strings.Join(subcommand, " ")) 121 | } 122 | return sub, nil 123 | } 124 | 125 | func (c *CLI) complete(compline string) error { 126 | fields := strings.Fields(compline) 127 | cmd, err := c.find(fields[1:]...) 128 | if err != nil { 129 | // If the command wasn't found, don't print anything 130 | return nil 131 | } 132 | for _, cmd := range cmd.commands { 133 | if cmd.hidden { 134 | continue 135 | } 136 | c.config.writer.Write([]byte(cmd.name + "\n")) 137 | } 138 | return nil 139 | } 140 | 141 | func trap(parent context.Context, signals ...os.Signal) context.Context { 142 | if len(signals) == 0 { 143 | return parent 144 | } 145 | ctx, stop := signal.NotifyContext(parent, signals...) 146 | // If context was canceled, stop catching signals 147 | go func() { 148 | <-ctx.Done() 149 | stop() 150 | }() 151 | return ctx 152 | } 153 | 154 | func lookupEnv(key *string) (string, bool) { 155 | if key == nil { 156 | return "", false 157 | } 158 | return os.LookupEnv(*key) 159 | } 160 | 161 | type missingInputError struct { 162 | Key string 163 | Env *string 164 | } 165 | 166 | func (m *missingInputError) Error() string { 167 | s := new(strings.Builder) 168 | s.WriteString("missing ") 169 | s.WriteString(m.Key) 170 | if m.Env != nil { 171 | s.WriteString(" or ") 172 | s.WriteString("$" + *m.Env) 173 | s.WriteString(" environment variable") 174 | } 175 | return s.String() 176 | } 177 | 178 | // If called from `go run` or `go test` don't trap any signals by default. This 179 | // avoids the "double Ctrl-C" problem where the user has to hit Ctrl-C twice to 180 | // exit the program. 181 | func defaultSignals() []os.Signal { 182 | exe, err := os.Executable() 183 | if err != nil { 184 | return []os.Signal{os.Interrupt} 185 | } 186 | if strings.Contains(exe, string(filepath.Separator)+"go-build") { 187 | return []os.Signal{} 188 | } 189 | // Otherwise, trap interrupts by default 190 | return []os.Signal{os.Interrupt} 191 | } 192 | -------------------------------------------------------------------------------- /usage.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "flag" 7 | "sort" 8 | "strconv" 9 | "strings" 10 | "text/tabwriter" 11 | "text/template" 12 | ) 13 | 14 | func Usage() error { 15 | return flag.ErrHelp 16 | } 17 | 18 | //go:embed usage.gotext 19 | var usageTemplate string 20 | 21 | var defaultUsage = template.Must(template.New("usage").Funcs(colors).Parse(usageTemplate)) 22 | 23 | type usage struct { 24 | cmd *command 25 | } 26 | 27 | func (u *usage) Name() string { 28 | return u.cmd.name 29 | } 30 | 31 | func (u *usage) Full() string { 32 | return u.cmd.full 33 | } 34 | 35 | func argIsOptional(arg *Arg) bool { 36 | if arg.value.optional() { 37 | return true 38 | } 39 | _, hasDefault := arg.value.Default() 40 | return hasDefault 41 | } 42 | 43 | func (u *usage) Usage() string { 44 | out := new(strings.Builder) 45 | if len(u.cmd.flags) > 0 { 46 | out.WriteString(" ") 47 | out.WriteString(dim()) 48 | out.WriteString("[flags]") 49 | out.WriteString(reset()) 50 | } 51 | if u.cmd.run != nil && len(u.cmd.args) > 0 { 52 | for _, arg := range u.cmd.args { 53 | isOptionalOrHasDefault := argIsOptional(arg) 54 | out.WriteString(" ") 55 | out.WriteString(dim()) 56 | if isOptionalOrHasDefault { 57 | out.WriteString("[") 58 | } 59 | out.WriteString("<") 60 | out.WriteString(arg.name) 61 | out.WriteString(">") 62 | if isOptionalOrHasDefault { 63 | out.WriteString("]") 64 | } 65 | out.WriteString(reset()) 66 | } 67 | } else if len(u.cmd.commands) > 0 { 68 | out.WriteString(" ") 69 | out.WriteString(dim()) 70 | out.WriteString("[command]") 71 | out.WriteString(reset()) 72 | } 73 | return out.String() 74 | } 75 | 76 | func (u *usage) Description() string { 77 | return u.cmd.help 78 | } 79 | 80 | func (u *usage) Args() (args usageArgs) { 81 | for _, arg := range u.cmd.args { 82 | args = append(args, &usageArg{arg}) 83 | } 84 | return args 85 | } 86 | 87 | type usageArg struct { 88 | a *Arg 89 | } 90 | 91 | func (a *usageArg) Suffix() string { 92 | if def, ok := a.a.value.Default(); ok { 93 | return " (default:" + strconv.Quote(def) + ")" 94 | } else if a.a.value.optional() { 95 | return " (optional)" 96 | } 97 | return "" 98 | } 99 | 100 | type usageArgs []*usageArg 101 | 102 | func (args usageArgs) Usage() (string, error) { 103 | buf := new(bytes.Buffer) 104 | tw := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) 105 | for _, arg := range args { 106 | tw.Write([]byte("\t\t<")) 107 | tw.Write([]byte(arg.a.name)) 108 | tw.Write([]byte(">")) 109 | if arg.a.help != "" { 110 | tw.Write([]byte("\t" + dim())) 111 | tw.Write([]byte(arg.a.help)) 112 | tw.Write([]byte(arg.Suffix())) 113 | tw.Write([]byte(reset())) 114 | } 115 | tw.Write([]byte("\n")) 116 | } 117 | if err := tw.Flush(); err != nil { 118 | return "", err 119 | } 120 | return strings.TrimSpace(buf.String()), nil 121 | } 122 | 123 | func (u *usage) Commands() (commands usageCommands) { 124 | for _, cmd := range u.cmd.commands { 125 | if cmd.advanced || cmd.hidden { 126 | continue 127 | } 128 | commands = append(commands, &usageCommand{cmd}) 129 | } 130 | // Sort by name 131 | sort.Slice(commands, func(i, j int) bool { 132 | return commands[i].c.name < commands[j].c.name 133 | }) 134 | return commands 135 | } 136 | 137 | func (u *usage) Advanced() (commands usageCommands) { 138 | for _, cmd := range u.cmd.commands { 139 | if !cmd.advanced || cmd.hidden { 140 | continue 141 | } 142 | commands = append(commands, &usageCommand{cmd}) 143 | } 144 | // Sort by name 145 | sort.Slice(commands, func(i, j int) bool { 146 | return commands[i].c.name < commands[j].c.name 147 | }) 148 | return commands 149 | } 150 | 151 | func (u *usage) Flags() (flags usageFlags) { 152 | flags = make(usageFlags, len(u.cmd.flags)) 153 | for i, flag := range u.cmd.flags { 154 | flags[i] = &usageFlag{flag} 155 | } 156 | // Sort by name 157 | sort.Slice(flags, func(i, j int) bool { 158 | if hasShort(flags[i]) == hasShort(flags[j]) { 159 | // Both have shorts or don't have shorts, so sort by name 160 | return flags[i].f.name < flags[j].f.name 161 | } 162 | // Shorts above non-shorts 163 | return flags[i].f.short > flags[j].f.short 164 | }) 165 | return flags 166 | } 167 | 168 | type usageCommand struct { 169 | c *command 170 | } 171 | 172 | type usageCommands []*usageCommand 173 | 174 | func (cmds usageCommands) Usage() (string, error) { 175 | buf := new(bytes.Buffer) 176 | tw := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) 177 | for _, cmd := range cmds { 178 | tw.Write([]byte("\t\t" + cmd.c.name)) 179 | if cmd.c.help != "" { 180 | tw.Write([]byte("\t" + dim() + cmd.c.help + reset())) 181 | } 182 | tw.Write([]byte("\n")) 183 | } 184 | if err := tw.Flush(); err != nil { 185 | return "", err 186 | } 187 | return strings.TrimSpace(buf.String()), nil 188 | } 189 | 190 | type usageFlag struct { 191 | f *Flag 192 | } 193 | 194 | func (u *usageFlag) Suffix() string { 195 | attrs := []string{} 196 | if u.f.env != nil { 197 | attrs = append(attrs, "or $"+*u.f.env) 198 | } 199 | if def, ok := u.f.value.Default(); ok { 200 | attrs = append(attrs, "default:"+strconv.Quote(def)) 201 | } else if u.f.value.optional() { 202 | attrs = append(attrs, "optional") 203 | } 204 | if len(attrs) == 0 { 205 | return "" 206 | } 207 | out := new(strings.Builder) 208 | out.WriteString(" (") 209 | for i, v := range attrs { 210 | if i > 0 { 211 | out.WriteString(", ") 212 | } 213 | out.WriteString(v) 214 | } 215 | out.WriteString(")") 216 | return out.String() 217 | } 218 | 219 | type usageFlags []*usageFlag 220 | 221 | func (flags usageFlags) Usage() (string, error) { 222 | buf := new(bytes.Buffer) 223 | tw := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) 224 | for _, flag := range flags { 225 | tw.Write([]byte("\t\t")) 226 | if flag.f.short != "" { 227 | tw.Write([]byte("-" + string(flag.f.short) + ", ")) 228 | } 229 | tw.Write([]byte("--" + flag.f.name)) 230 | if flag.f.help != "" { 231 | tw.Write([]byte("\t" + dim())) 232 | tw.Write([]byte(flag.f.help)) 233 | tw.Write([]byte(flag.Suffix())) 234 | tw.Write([]byte(reset())) 235 | } 236 | tw.Write([]byte("\n")) 237 | } 238 | if err := tw.Flush(); err != nil { 239 | return "", err 240 | } 241 | return strings.TrimSpace(buf.String()), nil 242 | } 243 | 244 | func hasShort(flag *usageFlag) bool { 245 | return flag.f.short != "" 246 | } 247 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= 6 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 7 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 8 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 9 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/hexops/autogold v0.8.1 h1:wvyd/bAJ+Dy+DcE09BoLk6r4Fa5R5W+O+GUzmR985WM= 13 | github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY= 14 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 15 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 16 | github.com/hexops/valast v1.4.1/go.mod h1:G+D6TExWuKs5he+hYlPMfYyhQ8w8qbc2vm4gDWwLdDg= 17 | github.com/hexops/valast v1.4.4 h1:rETyycw+/L2ZVJHHNxEBgh8KUn+87WugH9MxcEv9PGs= 18 | github.com/hexops/valast v1.4.4/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4= 19 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 20 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 21 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 22 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 23 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 24 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 25 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 26 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 27 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 28 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 29 | github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= 30 | github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= 31 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 32 | github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= 33 | github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 34 | github.com/matthewmueller/diff v0.0.3 h1:HNtVWIW/9mbM+7CKGW+k/tUr9ClFBO4Le+kS/5n2xDQ= 35 | github.com/matthewmueller/diff v0.0.3/go.mod h1:8V0QzU6IE7fmlOlE7NtXAeMkjB8cI9Cx4b1B7LNrz2U= 36 | github.com/matthewmueller/testchild v0.0.1 h1:llkDPWCc9I2pWwZ+tT3HWhWkv00RaF5+4Yn2/dLGT/Q= 37 | github.com/matthewmueller/testchild v0.0.1/go.mod h1:ZRwFgyiusaSAMlcuMFCU1NtZjs+o+QD7x/glsqXvB2Q= 38 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 42 | github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 43 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 44 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 45 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 46 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 47 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 48 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 51 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 52 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 53 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 54 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 55 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 56 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 57 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 58 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 62 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 65 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 66 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 67 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 73 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 74 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 75 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 76 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 79 | golang.org/x/tools v0.1.8-0.20211102182255-bb4add04ddef/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 80 | golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= 81 | golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= 82 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 84 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 88 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 89 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 90 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 91 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 92 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 93 | mvdan.cc/gofumpt v0.2.0/go.mod h1:TiGmrf914DAuT6+hDIxOqoDb4QXIzAuEUSXqEf9hGKY= 94 | mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= 95 | mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= 96 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "strings" 10 | ) 11 | 12 | func newCommand(config *config, flags []*Flag, name, full, help string) *command { 13 | fset := flag.NewFlagSet(name, flag.ContinueOnError) 14 | fset.SetOutput(io.Discard) 15 | return &command{ 16 | config: config, 17 | fset: fset, 18 | name: name, 19 | full: full, 20 | flags: flags, 21 | help: help, 22 | commands: map[string]*command{}, 23 | } 24 | } 25 | 26 | type command struct { 27 | config *config 28 | fset *flag.FlagSet 29 | stack []Middleware 30 | run func(ctx context.Context) error 31 | parsed bool 32 | 33 | // state for the template 34 | name string 35 | full string 36 | help string 37 | hidden bool 38 | advanced bool 39 | commands map[string]*command 40 | flags []*Flag 41 | args []*Arg 42 | restArgs *Args // optional, collects the rest of the args 43 | } 44 | 45 | var _ Command = (*command)(nil) 46 | 47 | func (c *command) printUsage() error { 48 | return c.config.usage.Execute(c.config.writer, &usage{c}) 49 | } 50 | 51 | type value interface { 52 | flag.Value 53 | optional() bool 54 | verify() error 55 | Default() (string, bool) 56 | } 57 | 58 | // Set flags only once 59 | func (c *command) setFlags() error { 60 | if c.parsed { 61 | return nil 62 | } 63 | c.parsed = true 64 | seen := map[string]bool{} 65 | for _, flag := range c.flags { 66 | if seen[flag.name] { 67 | return fmt.Errorf("%w %q command contains a duplicate flag \"--%s\"", ErrInvalidInput, c.full, flag.name) 68 | } else if flag.value == nil { 69 | return fmt.Errorf("%w %q command flag %q is missing a value setter", ErrInvalidInput, c.full, flag.name) 70 | } 71 | seen[flag.name] = true 72 | c.fset.Var(flag.value, flag.name, flag.help) 73 | if flag.short != "" { 74 | if seen[flag.short] { 75 | return fmt.Errorf("%w %q command contains a duplicate flag \"-%s\"", ErrInvalidInput, c.full, flag.short) 76 | } 77 | seen[flag.short] = true 78 | c.fset.Var(flag.value, flag.short, flag.help) 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | func (c *command) parse(ctx context.Context, args []string) error { 85 | // Set flags 86 | if err := c.setFlags(); err != nil { 87 | return err 88 | } 89 | 90 | // Stop at the first "--" argument 91 | var dashdash []string 92 | for i, arg := range args { 93 | if arg == "--" { 94 | dashdash = args[i:] 95 | args = args[:i] 96 | break 97 | } 98 | } 99 | 100 | // Parse the arguments 101 | if err := c.fset.Parse(args); err != nil { 102 | // Print usage if the developer used -h or --help 103 | if errors.Is(err, flag.ErrHelp) { 104 | return c.printUsage() 105 | } 106 | return maybeTrimError(err) 107 | } 108 | 109 | // Check if the first argument is a subcommand 110 | if sub, ok := c.commands[c.fset.Arg(0)]; ok { 111 | subArgs := c.fset.Args()[1:] 112 | if len(dashdash) > 0 { 113 | subArgs = append(subArgs, dashdash...) 114 | } 115 | return sub.parse(ctx, subArgs) 116 | } 117 | 118 | // Handle the remaining arguments 119 | numArgs := len(c.args) 120 | restArgs := c.fset.Args() 121 | 122 | // restArgs will start with an arg, so before parsing flags, check that the 123 | // command can handle additional args 124 | if len(restArgs) > 0 && len(c.args) == 0 && c.restArgs == nil { 125 | return fmt.Errorf("%w with unxpected arg %q", ErrInvalidInput, restArgs[0]) 126 | } 127 | 128 | // Also parse the flags after an arg 129 | restArgs, err := parseFlags(c.fset, restArgs) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | // Add anything after -- as a single argument 135 | if len(dashdash) > 0 { 136 | restArgs = append(restArgs, strings.Join(dashdash[1:], " ")) 137 | } 138 | 139 | loop: 140 | for i, arg := range restArgs { 141 | if i >= numArgs { 142 | if c.restArgs == nil { 143 | return fmt.Errorf("%w: %s", ErrInvalidInput, arg) 144 | } 145 | // Loop over the remaining unset args, appending them to restArgs 146 | if c.restArgs != nil { 147 | for _, arg := range restArgs[i:] { 148 | if err := c.restArgs.value.Set(arg); err != nil { 149 | return err 150 | } 151 | } 152 | } 153 | break loop 154 | } 155 | if err := c.args[i].value.Set(arg); err != nil { 156 | return err 157 | } 158 | } 159 | // Verify that all the args have been set or have default values 160 | if err := verifyArgs(c.args); err != nil { 161 | return err 162 | } 163 | // Also verify rest args if we have any 164 | if c.restArgs != nil { 165 | if err := c.restArgs.verify(); err != nil { 166 | return err 167 | } 168 | } 169 | // Verify that all the flags have been set or have default values 170 | if err := verifyFlags(c.flags); err != nil { 171 | return err 172 | } 173 | // Print usage if there's no run function defined 174 | if c.run == nil { 175 | if len(restArgs) == 0 { 176 | return c.printUsage() 177 | } 178 | return fmt.Errorf("%w: %s", ErrInvalidInput, c.fset.Arg(0)) 179 | } 180 | 181 | // Compose the middlewares 182 | run := compose(c.run, c.stack...) 183 | 184 | // Run the command 185 | if err := run(ctx); err != nil { 186 | // Support explicitly printing usage 187 | if errors.Is(err, flag.ErrHelp) { 188 | return c.printUsage() 189 | } 190 | return err 191 | } 192 | return nil 193 | } 194 | 195 | func (c *command) Run(runner func(ctx context.Context) error) { 196 | c.run = runner 197 | } 198 | 199 | func (c *command) Use(middlewares ...Middleware) Command { 200 | c.stack = append(c.stack, middlewares...) 201 | return c 202 | } 203 | 204 | func (c *command) Command(name, help string) Command { 205 | if c.commands[name] != nil { 206 | return c.commands[name] 207 | } 208 | // Copy the flags from the parent command 209 | flags := append([]*Flag{}, c.flags...) 210 | // Create the subcommand 211 | cmd := newCommand(c.config, flags, name, c.full+" "+name, help) 212 | c.commands[name] = cmd 213 | return cmd 214 | } 215 | 216 | func (c *command) Hidden() Command { 217 | c.hidden = true 218 | return c 219 | } 220 | 221 | func (c *command) Advanced() Command { 222 | c.advanced = true 223 | return c 224 | } 225 | 226 | func (c *command) Arg(name, help string) *Arg { 227 | arg := &Arg{ 228 | name: name, 229 | help: help, 230 | } 231 | c.args = append(c.args, arg) 232 | return arg 233 | } 234 | 235 | func (c *command) Args(name, help string) *Args { 236 | if c.restArgs != nil { 237 | // Panic is okay here because settings commands should be done during 238 | // initialization. We want to fail fast for invalid usage. 239 | panic("commander: you can only use cmd.Args(name, usage) once per command") 240 | } 241 | args := &Args{ 242 | name: name, 243 | help: help, 244 | } 245 | c.restArgs = args 246 | return args 247 | } 248 | 249 | func (c *command) Flag(name, help string) *Flag { 250 | flag := &Flag{ 251 | name: name, 252 | help: help, 253 | } 254 | c.flags = append(c.flags, flag) 255 | return flag 256 | } 257 | 258 | func (c *command) Find(cmds ...string) (*command, bool) { 259 | if len(cmds) == 0 { 260 | return c, true 261 | } 262 | cmd := cmds[0] 263 | sub, ok := c.commands[cmd] 264 | if !ok { 265 | return nil, false 266 | } 267 | return sub.Find(cmds[1:]...) 268 | } 269 | 270 | func isFlag(arg string) bool { 271 | return strings.HasPrefix(arg, "-") && strings.TrimLeft(arg, "-") != "" 272 | } 273 | 274 | func parseFlags(fset *flag.FlagSet, args []string) (rest []string, err error) { 275 | for i, arg := range args { 276 | if !isFlag(arg) { 277 | rest = append(rest, arg) 278 | continue 279 | } 280 | if err := fset.Parse(args[i:]); err != nil { 281 | return nil, err 282 | } 283 | remaining, err := parseFlags(fset, fset.Args()) 284 | if err != nil { 285 | return nil, err 286 | } 287 | rest = append(rest, remaining...) 288 | return rest, nil 289 | } 290 | return rest, nil 291 | } 292 | 293 | // This is a hack to trim the error messages returned by the flag package. 294 | func maybeTrimError(err error) error { 295 | msg := err.Error() 296 | idx := -1 297 | if strings.HasPrefix(msg, "invalid value ") { 298 | idx = maybeTrimInvalidValue(msg) 299 | } else if strings.HasPrefix(msg, "invalid boolean value ") { 300 | idx = maybeTrimInvalidBooleanValue(msg) 301 | } 302 | if idx < 0 { 303 | return err 304 | } 305 | return errors.New(msg[idx:]) 306 | } 307 | 308 | func maybeTrimInvalidValue(msg string) int { 309 | i1 := strings.Index(msg, `" for flag -`) 310 | if i1 < 0 { 311 | return i1 312 | } 313 | i1 += 12 314 | i2 := strings.Index(msg[i1:], ": ") 315 | if i2 < 0 { 316 | return i2 317 | } 318 | i2 += 2 319 | return i1 + i2 320 | } 321 | 322 | func maybeTrimInvalidBooleanValue(msg string) int { 323 | i1 := strings.Index(msg, `" for -`) 324 | if i1 < 0 { 325 | return i1 326 | } 327 | i1 += 7 328 | i2 := strings.Index(msg[i1:], `: `) 329 | if i2 < 0 { 330 | return i2 331 | } 332 | i2 += 2 333 | return i1 + i2 334 | } 335 | 336 | func compose(run func(ctx context.Context) error, middlewares ...Middleware) func(ctx context.Context) error { 337 | if run == nil { 338 | return nil 339 | } 340 | // apply in reverse so that the first middleware in the slice is executed first 341 | for i := len(middlewares) - 1; i >= 0; i-- { 342 | run = middlewares[i](run) 343 | } 344 | return run 345 | } 346 | -------------------------------------------------------------------------------- /cli_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "errors" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/livebud/cli" 16 | "github.com/matryer/is" 17 | "github.com/matthewmueller/diff" 18 | "github.com/matthewmueller/testchild" 19 | ) 20 | 21 | func isEqual(t testing.TB, actual, expected string) { 22 | t.Helper() 23 | equal(t, expected, replaceEscapeCodes(actual)) 24 | } 25 | 26 | func replaceEscapeCodes(str string) string { 27 | r := strings.NewReplacer( 28 | "\033[0m", `{reset}`, 29 | "\033[1m", `{bold}`, 30 | "\033[37m", `{dim}`, 31 | "\033[4m", `{underline}`, 32 | "\033[36m", `{teal}`, 33 | "\033[34m", `{blue}`, 34 | "\033[33m", `{yellow}`, 35 | "\033[31m", `{red}`, 36 | "\033[32m", `{green}`, 37 | ) 38 | return r.Replace(str) 39 | } 40 | 41 | // is checks if expect and actual are equal 42 | func equal(t testing.TB, expect, actual string) { 43 | t.Helper() 44 | if expect == actual { 45 | return 46 | } 47 | t.Fatal(diff.String(expect, actual)) 48 | } 49 | 50 | func encode(w io.Writer, cmd, in any) error { 51 | return json.NewEncoder(w).Encode(map[string]any{ 52 | "cmd": cmd, 53 | "in": in, 54 | }) 55 | } 56 | 57 | func heroku(w io.Writer) *cli.CLI { 58 | type global struct { 59 | App string 60 | Remote *string 61 | } 62 | var g global 63 | cli := cli.New("heroku", `CLI to interact with Heroku`).Writer(w) 64 | cli.Flag("app", "app to run command against").Short('a').String(&g.App) 65 | cli.Flag("remote", "git remote of app to use").Short('r').Optional().String(&g.Remote) 66 | 67 | { 68 | var in = struct { 69 | *global 70 | All bool 71 | Json bool 72 | }{global: &g} 73 | cli := cli.Command("addons", `lists your add-ons and attachments`) 74 | cli.Flag("all", "show add-ons and attachments for all accessible apps").Bool(&in.All).Default(false) 75 | cli.Flag("json", "return add-ons in json format").Bool(&in.Json).Default(false) 76 | cli.Run(func(ctx context.Context) error { return encode(w, "addons", in) }) 77 | 78 | { 79 | var in = struct { 80 | *global 81 | Name *string 82 | As *string 83 | }{global: &g} 84 | cli := cli.Command("attach", `attach an existing add-on resource to an app`) 85 | cli.Flag("name", "name for the add-on resource").Optional().String(&in.Name) 86 | cli.Flag("as", "name for add-on attachment").Optional().String(&in.As) 87 | cli.Run(func(ctx context.Context) error { return encode(w, "addons:attach", in) }) 88 | } 89 | 90 | { 91 | var in = struct { 92 | *global 93 | Name *string 94 | As *string 95 | Wait bool 96 | }{global: &g} 97 | cli := cli.Command("create", `create a new add-on resource and attach to an app`) 98 | cli.Flag("name", "name for the add-on resource").Optional().String(&in.Name) 99 | cli.Flag("as", "name for add-on attachment").Optional().String(&in.As) 100 | cli.Flag("wait", "watch add-on creation status and exit when complete").Bool(&in.Wait).Default(false) 101 | cli.Run(func(ctx context.Context) error { return encode(w, "addons:create", in) }) 102 | } 103 | 104 | { 105 | var in = struct { 106 | global 107 | Force bool 108 | }{global: g} 109 | cli := cli.Command("destroy", `destroy permanently destroys an add-on resource`) 110 | cli.Flag("force", "force destroy").Bool(&in.Force).Default(false) 111 | cli.Run(func(ctx context.Context) error { return encode(w, "addons:destroy", in) }) 112 | } 113 | 114 | { 115 | var in = struct { 116 | *global 117 | }{global: &g} 118 | cli := cli.Command("info", `show detailed information for an add-on`) 119 | cli.Run(func(ctx context.Context) error { return encode(w, "addons:info", in) }) 120 | } 121 | } 122 | 123 | { 124 | var in = struct { 125 | *global 126 | Json bool 127 | }{global: &g} 128 | cli := cli.Command("ps", `list dynos for an app`) 129 | cli.Flag("json", "output in json format").Bool(&in.Json).Default(false) 130 | cli.Run(func(ctx context.Context) error { return encode(w, "ps", in) }) 131 | 132 | { 133 | var in = struct { 134 | *global 135 | Value string 136 | }{global: &g} 137 | cli := cli.Command("scale", `scale dyno quantity up or down`) 138 | cli.Arg("value", "some value").String(&in.Value) 139 | cli.Run(func(ctx context.Context) error { 140 | return encode(w, "ps:scale", in) 141 | }) 142 | } 143 | 144 | { 145 | cli := cli.Command("autoscale", `enable autoscaling for an app`) 146 | 147 | { 148 | var in = struct { 149 | *global 150 | Min int 151 | Max int 152 | Notifications bool 153 | P95 int 154 | }{global: &g} 155 | cli := cli.Command("enable", `enable autoscaling for an app`) 156 | cli.Flag("min", "minimum number of dynos").Int(&in.Min) 157 | cli.Flag("max", "maximum number of dynos").Int(&in.Max) 158 | cli.Flag("notifications", "comma-separated list of notifications to enable").Bool(&in.Notifications) 159 | cli.Flag("p95", "95th percentile response time threshold").Int(&in.P95) 160 | cli.Run(func(ctx context.Context) error { return encode(w, "ps:autoscale:enable", in) }) 161 | } 162 | 163 | { 164 | var in = struct { 165 | *global 166 | }{global: &g} 167 | cli := cli.Command("disable", `disable autoscaling for an app`) 168 | cli.Run(func(ctx context.Context) error { return encode(w, "ps:autoscale:disable", in) }) 169 | } 170 | } 171 | } 172 | 173 | return cli 174 | } 175 | 176 | func TestHerokuHelp(t *testing.T) { 177 | is := is.New(t) 178 | actual := new(bytes.Buffer) 179 | cli := heroku(actual) 180 | ctx := context.Background() 181 | err := cli.Parse(ctx, "-h") 182 | is.NoErr(err) 183 | isEqual(t, actual.String(), ` 184 | {bold}Usage:{reset} 185 | {dim}${reset} heroku {dim}[flags]{reset} {dim}[command]{reset} 186 | 187 | {bold}Description:{reset} 188 | CLI to interact with Heroku 189 | 190 | {bold}Flags:{reset} 191 | -a, --app {dim}app to run command against{reset} 192 | -r, --remote {dim}git remote of app to use (optional){reset} 193 | 194 | {bold}Commands:{reset} 195 | addons {dim}lists your add-ons and attachments{reset} 196 | ps {dim}list dynos for an app{reset} 197 | 198 | `) 199 | } 200 | 201 | func TestHerokuHelpPs(t *testing.T) { 202 | is := is.New(t) 203 | actual := new(bytes.Buffer) 204 | cli := heroku(actual) 205 | ctx := context.Background() 206 | err := cli.Parse(ctx, "ps", "-h") 207 | is.NoErr(err) 208 | isEqual(t, actual.String(), ` 209 | {bold}Usage:{reset} 210 | {dim}${reset} heroku ps {dim}[flags]{reset} {dim}[command]{reset} 211 | 212 | {bold}Description:{reset} 213 | list dynos for an app 214 | 215 | {bold}Flags:{reset} 216 | -a, --app {dim}app to run command against{reset} 217 | -r, --remote {dim}git remote of app to use (optional){reset} 218 | --json {dim}output in json format (default:"false"){reset} 219 | 220 | {bold}Commands:{reset} 221 | autoscale {dim}enable autoscaling for an app{reset} 222 | scale {dim}scale dyno quantity up or down{reset} 223 | 224 | `) 225 | } 226 | 227 | func TestHerokuHelpPsAutoscale(t *testing.T) { 228 | is := is.New(t) 229 | actual := new(bytes.Buffer) 230 | cli := heroku(actual) 231 | ctx := context.Background() 232 | err := cli.Parse(ctx, "ps", "autoscale", "-h") 233 | is.NoErr(err) 234 | isEqual(t, actual.String(), ` 235 | {bold}Usage:{reset} 236 | {dim}${reset} heroku ps autoscale {dim}[flags]{reset} {dim}[command]{reset} 237 | 238 | {bold}Description:{reset} 239 | enable autoscaling for an app 240 | 241 | {bold}Flags:{reset} 242 | -a, --app {dim}app to run command against{reset} 243 | -r, --remote {dim}git remote of app to use (optional){reset} 244 | --json {dim}output in json format (default:"false"){reset} 245 | 246 | {bold}Commands:{reset} 247 | disable {dim}disable autoscaling for an app{reset} 248 | enable {dim}enable autoscaling for an app{reset} 249 | 250 | `) 251 | } 252 | 253 | func TestHerokuHelpPsAutoscaleEnable(t *testing.T) { 254 | is := is.New(t) 255 | actual := new(bytes.Buffer) 256 | cli := heroku(actual) 257 | ctx := context.Background() 258 | err := cli.Parse(ctx, "ps", "autoscale", "enable", "-h") 259 | is.NoErr(err) 260 | isEqual(t, actual.String(), ` 261 | {bold}Usage:{reset} 262 | {dim}${reset} heroku ps autoscale enable {dim}[flags]{reset} 263 | 264 | {bold}Description:{reset} 265 | enable autoscaling for an app 266 | 267 | {bold}Flags:{reset} 268 | -a, --app {dim}app to run command against{reset} 269 | -r, --remote {dim}git remote of app to use (optional){reset} 270 | --json {dim}output in json format (default:"false"){reset} 271 | --max {dim}maximum number of dynos{reset} 272 | --min {dim}minimum number of dynos{reset} 273 | --notifications {dim}comma-separated list of notifications to enable{reset} 274 | --p95 {dim}95th percentile response time threshold{reset} 275 | 276 | `) 277 | } 278 | 279 | // Possible signatures: 280 | // cli [flags|args] 281 | // cli [sub] [flags|args] 282 | 283 | func TestHerokuFlagArgOrder(t *testing.T) { 284 | is := is.New(t) 285 | actual := new(bytes.Buffer) 286 | cli := heroku(actual) 287 | ctx := context.Background() 288 | err := cli.Parse(ctx, "ps", "scale", "--app=foo", "--remote", "bar", "web=1") 289 | is.NoErr(err) 290 | is.Equal(actual.String(), `{"cmd":"ps:scale","in":{"App":"foo","Remote":"bar","Value":"web=1"}}`+"\n") 291 | } 292 | 293 | func TestHerokuArgFlagOutOfOrder(t *testing.T) { 294 | is := is.New(t) 295 | actual := new(bytes.Buffer) 296 | cmd := heroku(actual) 297 | ctx := context.Background() 298 | err := cmd.Parse(ctx, "ps", "scale", "web=1", "--app=foo", "--remote", "bar") 299 | is.NoErr(err) 300 | is.Equal(actual.String(), `{"cmd":"ps:scale","in":{"App":"foo","Remote":"bar","Value":"web=1"}}`+"\n") 301 | } 302 | 303 | func TestHerokuInvalidFlagArgFlagOrderTwo(t *testing.T) { 304 | is := is.New(t) 305 | actual := new(bytes.Buffer) 306 | cmd := heroku(actual) 307 | ctx := context.Background() 308 | err := cmd.Parse(ctx, "ps:scale", "--app=foo", "web=1", "--remote", "bar") 309 | is.True(errors.Is(err, cli.ErrInvalidInput)) 310 | } 311 | 312 | func TestHelpArgs(t *testing.T) { 313 | is := is.New(t) 314 | actual := new(bytes.Buffer) 315 | cmd := cli.New("cp", "copy files").Writer(actual) 316 | cmd.Arg("src", "source").String(nil) 317 | cmd.Arg("dst", "destination").String(nil).Default(".") 318 | cmd.Run(func(ctx context.Context) error { return nil }) 319 | ctx := context.Background() 320 | err := cmd.Parse(ctx, "-h") 321 | is.NoErr(err) 322 | isEqual(t, actual.String(), ` 323 | {bold}Usage:{reset} 324 | {dim}${reset} cp {dim}{reset} {dim}[]{reset} 325 | 326 | {bold}Description:{reset} 327 | copy files 328 | 329 | {bold}Args:{reset} 330 | {dim}source{reset} 331 | {dim}destination (default:"."){reset} 332 | 333 | `) 334 | } 335 | 336 | func TestInvalid(t *testing.T) { 337 | is := is.New(t) 338 | actual := new(bytes.Buffer) 339 | cmd := cli.New("cli", "desc").Writer(actual) 340 | ctx := context.Background() 341 | err := cmd.Parse(ctx, "blargle") 342 | is.True(err != nil) 343 | is.True(errors.Is(err, cli.ErrInvalidInput)) 344 | isEqual(t, actual.String(), ``) 345 | } 346 | 347 | func TestSimple(t *testing.T) { 348 | is := is.New(t) 349 | actual := new(bytes.Buffer) 350 | cli := cli.New("cli", "desc").Writer(actual) 351 | called := 0 352 | cli.Run(func(ctx context.Context) error { 353 | called++ 354 | return nil 355 | }) 356 | ctx := context.Background() 357 | err := cli.Parse(ctx) 358 | is.NoErr(err) 359 | is.Equal(1, called) 360 | isEqual(t, actual.String(), ``) 361 | } 362 | 363 | func TestFlagString(t *testing.T) { 364 | is := is.New(t) 365 | actual := new(bytes.Buffer) 366 | called := 0 367 | cli := cli.New("cli", "desc").Writer(actual) 368 | cli.Run(func(ctx context.Context) error { 369 | called++ 370 | return nil 371 | }) 372 | var flag string 373 | cli.Flag("flag", "cli flag").String(&flag) 374 | ctx := context.Background() 375 | err := cli.Parse(ctx, "--flag", "cool") 376 | is.NoErr(err) 377 | is.Equal(1, called) 378 | is.Equal(flag, "cool") 379 | isEqual(t, actual.String(), ``) 380 | } 381 | 382 | func TestFlagStringDefault(t *testing.T) { 383 | is := is.New(t) 384 | actual := new(bytes.Buffer) 385 | called := 0 386 | cli := cli.New("cli", "desc").Writer(actual) 387 | cli.Run(func(ctx context.Context) error { 388 | called++ 389 | return nil 390 | }) 391 | var flag string 392 | cli.Flag("flag", "cli flag").String(&flag).Default("default") 393 | ctx := context.Background() 394 | err := cli.Parse(ctx) 395 | is.NoErr(err) 396 | is.Equal(1, called) 397 | is.Equal(flag, "default") 398 | isEqual(t, actual.String(), ``) 399 | } 400 | 401 | func TestFlagStringEmptyDefault(t *testing.T) { 402 | is := is.New(t) 403 | actual := new(bytes.Buffer) 404 | called := 0 405 | cli := cli.New("cli", "desc").Writer(actual) 406 | cli.Run(func(ctx context.Context) error { 407 | called++ 408 | return nil 409 | }) 410 | var flag string 411 | cli.Flag("flag", "cli flag").String(&flag).Default("") 412 | ctx := context.Background() 413 | err := cli.Parse(ctx, "-h") 414 | is.NoErr(err) 415 | isEqual(t, actual.String(), ` 416 | {bold}Usage:{reset} 417 | {dim}${reset} cli {dim}[flags]{reset} 418 | 419 | {bold}Description:{reset} 420 | desc 421 | 422 | {bold}Flags:{reset} 423 | --flag {dim}cli flag (default:""){reset} 424 | 425 | `) 426 | } 427 | 428 | func TestFlagStringRequired(t *testing.T) { 429 | is := is.New(t) 430 | actual := new(bytes.Buffer) 431 | called := 0 432 | cli := cli.New("cli", "desc").Writer(actual) 433 | cli.Run(func(ctx context.Context) error { 434 | called++ 435 | return nil 436 | }) 437 | var flag string 438 | cli.Flag("flag", "cli flag").Env("$FLAG").String(&flag) 439 | ctx := context.Background() 440 | err := cli.Parse(ctx) 441 | is.True(err != nil) 442 | is.Equal(err.Error(), "missing --flag or $FLAG environment variable") 443 | } 444 | 445 | func TestFlagInt(t *testing.T) { 446 | is := is.New(t) 447 | actual := new(bytes.Buffer) 448 | called := 0 449 | cli := cli.New("cli", "desc").Writer(actual) 450 | cli.Run(func(ctx context.Context) error { 451 | called++ 452 | return nil 453 | }) 454 | var flag int 455 | cli.Flag("flag", "cli flag").Int(&flag) 456 | ctx := context.Background() 457 | err := cli.Parse(ctx, "--flag", "10") 458 | is.NoErr(err) 459 | is.Equal(1, called) 460 | is.Equal(flag, 10) 461 | isEqual(t, actual.String(), ``) 462 | } 463 | 464 | func TestFlagIntInvalid(t *testing.T) { 465 | is := is.New(t) 466 | actual := new(bytes.Buffer) 467 | called := 0 468 | cli := cli.New("cli", "desc").Writer(actual) 469 | cli.Run(func(ctx context.Context) error { 470 | called++ 471 | return nil 472 | }) 473 | var flag int 474 | cli.Flag("flag", "cli flag").Int(&flag) 475 | ctx := context.Background() 476 | err := cli.Parse(ctx, "--flag", "hi") 477 | is.True(err != nil) 478 | is.Equal(err.Error(), `--flag: expected an integer but got "hi"`) 479 | } 480 | 481 | func TestFlagOptionalIntInvalid(t *testing.T) { 482 | is := is.New(t) 483 | actual := new(bytes.Buffer) 484 | called := 0 485 | cli := cli.New("cli", "desc").Writer(actual) 486 | cli.Run(func(ctx context.Context) error { 487 | called++ 488 | return nil 489 | }) 490 | var flag *int 491 | cli.Flag("flag", "cli flag").Optional().Int(&flag) 492 | ctx := context.Background() 493 | err := cli.Parse(ctx, "--flag", "hi") 494 | is.True(err != nil) 495 | is.Equal(err.Error(), `--flag: expected an integer but got "hi"`) 496 | } 497 | 498 | func TestFlagIntDefault(t *testing.T) { 499 | is := is.New(t) 500 | actual := new(bytes.Buffer) 501 | called := 0 502 | cli := cli.New("cli", "desc").Writer(actual) 503 | cli.Run(func(ctx context.Context) error { 504 | called++ 505 | return nil 506 | }) 507 | var flag int 508 | cli.Flag("flag", "cli flag").Int(&flag).Default(10) 509 | ctx := context.Background() 510 | err := cli.Parse(ctx) 511 | is.NoErr(err) 512 | is.Equal(1, called) 513 | is.Equal(flag, 10) 514 | isEqual(t, actual.String(), ``) 515 | } 516 | 517 | func TestFlagIntRequired(t *testing.T) { 518 | is := is.New(t) 519 | actual := new(bytes.Buffer) 520 | called := 0 521 | cli := cli.New("cli", "desc").Writer(actual) 522 | cli.Run(func(ctx context.Context) error { 523 | called++ 524 | return nil 525 | }) 526 | var flag int 527 | cli.Flag("flag", "cli flag").Int(&flag) 528 | ctx := context.Background() 529 | err := cli.Parse(ctx) 530 | is.True(err != nil) 531 | is.Equal(err.Error(), "missing --flag") 532 | } 533 | 534 | func TestFlagBool(t *testing.T) { 535 | is := is.New(t) 536 | actual := new(bytes.Buffer) 537 | called := 0 538 | cli := cli.New("cli", "desc").Writer(actual) 539 | cli.Run(func(ctx context.Context) error { 540 | called++ 541 | return nil 542 | }) 543 | var flag bool 544 | cli.Flag("flag", "cli flag").Bool(&flag) 545 | ctx := context.Background() 546 | err := cli.Parse(ctx, "--flag") 547 | is.NoErr(err) 548 | is.Equal(1, called) 549 | is.Equal(flag, true) 550 | isEqual(t, actual.String(), ``) 551 | } 552 | 553 | func TestFlagBoolDefault(t *testing.T) { 554 | is := is.New(t) 555 | actual := new(bytes.Buffer) 556 | called := 0 557 | cli := cli.New("cli", "desc").Writer(actual) 558 | cli.Run(func(ctx context.Context) error { 559 | called++ 560 | return nil 561 | }) 562 | var flag bool 563 | cli.Flag("flag", "cli flag").Bool(&flag).Default(true) 564 | ctx := context.Background() 565 | err := cli.Parse(ctx) 566 | is.NoErr(err) 567 | is.Equal(1, called) 568 | is.Equal(flag, true) 569 | isEqual(t, actual.String(), ``) 570 | } 571 | 572 | func TestFlagBoolDefaultFalse(t *testing.T) { 573 | is := is.New(t) 574 | actual := new(bytes.Buffer) 575 | called := 0 576 | cli := cli.New("cli", "desc").Writer(actual) 577 | cli.Run(func(ctx context.Context) error { 578 | called++ 579 | return nil 580 | }) 581 | var flag bool 582 | cli.Flag("flag", "cli flag").Bool(&flag).Default(true) 583 | ctx := context.Background() 584 | err := cli.Parse(ctx, "--flag=false") 585 | is.NoErr(err) 586 | is.Equal(1, called) 587 | is.Equal(flag, false) 588 | isEqual(t, actual.String(), ``) 589 | } 590 | 591 | func TestFlagBoolRequired(t *testing.T) { 592 | is := is.New(t) 593 | actual := new(bytes.Buffer) 594 | called := 0 595 | cli := cli.New("cli", "desc").Writer(actual) 596 | cli.Run(func(ctx context.Context) error { 597 | called++ 598 | return nil 599 | }) 600 | var flag bool 601 | cli.Flag("flag", "cli flag").Bool(&flag) 602 | ctx := context.Background() 603 | err := cli.Parse(ctx) 604 | is.True(err != nil) 605 | is.Equal(err.Error(), "missing --flag") 606 | } 607 | 608 | func TestFlagBoolInvalid(t *testing.T) { 609 | is := is.New(t) 610 | actual := new(bytes.Buffer) 611 | called := 0 612 | cli := cli.New("cli", "desc").Writer(actual) 613 | cli.Run(func(ctx context.Context) error { 614 | called++ 615 | return nil 616 | }) 617 | var flag bool 618 | cli.Flag("flag", "cli flag").Bool(&flag) 619 | ctx := context.Background() 620 | err := cli.Parse(ctx, "--flag=hi") 621 | is.True(err != nil) 622 | is.Equal(err.Error(), `--flag: expected a boolean but got "hi"`) 623 | } 624 | 625 | func TestFlagOptionalBoolInvalid(t *testing.T) { 626 | is := is.New(t) 627 | actual := new(bytes.Buffer) 628 | called := 0 629 | cli := cli.New("cli", "desc").Writer(actual) 630 | cli.Run(func(ctx context.Context) error { 631 | called++ 632 | return nil 633 | }) 634 | var flag *bool 635 | cli.Flag("flag", "cli flag").Optional().Bool(&flag) 636 | ctx := context.Background() 637 | err := cli.Parse(ctx, "--flag=hi") 638 | is.True(err != nil) 639 | is.Equal(err.Error(), `--flag: expected a boolean but got "hi"`) 640 | } 641 | 642 | func TestFlagStrings(t *testing.T) { 643 | is := is.New(t) 644 | actual := new(bytes.Buffer) 645 | called := 0 646 | cli := cli.New("cli", "desc").Writer(actual) 647 | cli.Run(func(ctx context.Context) error { 648 | called++ 649 | return nil 650 | }) 651 | var flags []string 652 | cli.Flag("flag", "cli flag").Strings(&flags) 653 | ctx := context.Background() 654 | err := cli.Parse(ctx, "--flag", "1", "--flag", "2") 655 | is.NoErr(err) 656 | is.Equal(len(flags), 2) 657 | is.Equal(flags[0], "1") 658 | is.Equal(flags[1], "2") 659 | } 660 | 661 | func TestFlagStringsRequired(t *testing.T) { 662 | is := is.New(t) 663 | actual := new(bytes.Buffer) 664 | called := 0 665 | cli := cli.New("cli", "desc").Writer(actual) 666 | cli.Run(func(ctx context.Context) error { 667 | called++ 668 | return nil 669 | }) 670 | var flags []string 671 | cli.Flag("flag", "cli flag").Strings(&flags) 672 | ctx := context.Background() 673 | err := cli.Parse(ctx) 674 | is.True(err != nil) 675 | is.Equal(err.Error(), "missing --flag") 676 | } 677 | 678 | func TestFlagStringsDefault(t *testing.T) { 679 | is := is.New(t) 680 | actual := new(bytes.Buffer) 681 | called := 0 682 | cli := cli.New("cli", "desc").Writer(actual) 683 | cli.Run(func(ctx context.Context) error { 684 | called++ 685 | return nil 686 | }) 687 | var flags []string 688 | cli.Flag("flag", "cli flag").Strings(&flags).Default("a", "b") 689 | ctx := context.Background() 690 | err := cli.Parse(ctx) 691 | is.NoErr(err) 692 | is.Equal(len(flags), 2) 693 | is.Equal(flags[0], "a") 694 | is.Equal(flags[1], "b") 695 | } 696 | 697 | func TestFlagStringsEmptyDefault(t *testing.T) { 698 | is := is.New(t) 699 | actual := new(bytes.Buffer) 700 | called := 0 701 | cli := cli.New("cli", "desc").Writer(actual) 702 | cli.Run(func(ctx context.Context) error { 703 | called++ 704 | return nil 705 | }) 706 | var flags []string 707 | cli.Flag("flag", "cli flag").Strings(&flags).Default() 708 | ctx := context.Background() 709 | err := cli.Parse(ctx) 710 | is.NoErr(err) 711 | is.Equal(len(flags), 0) 712 | } 713 | 714 | func TestFlagStringsEmptyDefaultHelp(t *testing.T) { 715 | is := is.New(t) 716 | actual := new(bytes.Buffer) 717 | called := 0 718 | cli := cli.New("cli", "desc").Writer(actual) 719 | cli.Run(func(ctx context.Context) error { 720 | called++ 721 | return nil 722 | }) 723 | var flags []string 724 | cli.Flag("flag", "cli flag").Strings(&flags).Default() 725 | ctx := context.Background() 726 | err := cli.Parse(ctx, "-h") 727 | is.NoErr(err) 728 | isEqual(t, actual.String(), ` 729 | {bold}Usage:{reset} 730 | {dim}${reset} cli {dim}[flags]{reset} 731 | 732 | {bold}Description:{reset} 733 | desc 734 | 735 | {bold}Flags:{reset} 736 | --flag {dim}cli flag (default:"[]"){reset} 737 | 738 | `) 739 | } 740 | 741 | func TestFlagStringMap(t *testing.T) { 742 | is := is.New(t) 743 | actual := new(bytes.Buffer) 744 | called := 0 745 | cli := cli.New("cli", "desc").Writer(actual) 746 | cli.Run(func(ctx context.Context) error { 747 | called++ 748 | return nil 749 | }) 750 | var flags map[string]string 751 | cli.Flag("flag", "cli flag").StringMap(&flags) 752 | ctx := context.Background() 753 | err := cli.Parse(ctx, "--flag", "a:1 + 1", "--flag", "b:2") 754 | is.NoErr(err) 755 | is.Equal(len(flags), 2) 756 | is.Equal(flags["a"], "1 + 1") 757 | is.Equal(flags["b"], "2") 758 | } 759 | 760 | func TestFlagStringMapRequired(t *testing.T) { 761 | is := is.New(t) 762 | actual := new(bytes.Buffer) 763 | called := 0 764 | cli := cli.New("cli", "desc").Writer(actual) 765 | cli.Run(func(ctx context.Context) error { 766 | called++ 767 | return nil 768 | }) 769 | var flags map[string]string 770 | cli.Flag("flag", "cli flag").StringMap(&flags) 771 | ctx := context.Background() 772 | err := cli.Parse(ctx) 773 | is.True(err != nil) 774 | is.Equal(err.Error(), "missing --flag") 775 | } 776 | 777 | func TestFlagStringMapOptional(t *testing.T) { 778 | is := is.New(t) 779 | actual := new(bytes.Buffer) 780 | called := 0 781 | cli := cli.New("cli", "desc").Writer(actual) 782 | cli.Run(func(ctx context.Context) error { 783 | called++ 784 | return nil 785 | }) 786 | var flags map[string]string 787 | cli.Flag("flag", "cli flag").Optional().StringMap(&flags) 788 | ctx := context.Background() 789 | err := cli.Parse(ctx) 790 | is.NoErr(err) 791 | is.Equal(len(flags), 0) 792 | } 793 | 794 | func TestFlagStringMapDefault(t *testing.T) { 795 | is := is.New(t) 796 | actual := new(bytes.Buffer) 797 | called := 0 798 | cli := cli.New("cli", "desc").Writer(actual) 799 | cli.Run(func(ctx context.Context) error { 800 | called++ 801 | return nil 802 | }) 803 | var flags map[string]string 804 | cli.Flag("flag", "cli flag").StringMap(&flags).Default(map[string]string{ 805 | "a": "1", 806 | "b": "2", 807 | }) 808 | ctx := context.Background() 809 | err := cli.Parse(ctx) 810 | is.NoErr(err) 811 | is.Equal(len(flags), 2) 812 | is.Equal(flags["a"], "1") 813 | is.Equal(flags["b"], "2") 814 | } 815 | 816 | func TestFlagStringMapEmptyDefault(t *testing.T) { 817 | is := is.New(t) 818 | actual := new(bytes.Buffer) 819 | called := 0 820 | cli := cli.New("cli", "desc").Writer(actual) 821 | cli.Run(func(ctx context.Context) error { 822 | called++ 823 | return nil 824 | }) 825 | var flags map[string]string 826 | cli.Flag("flag", "cli flag").StringMap(&flags).Default(map[string]string{}) 827 | ctx := context.Background() 828 | err := cli.Parse(ctx) 829 | is.NoErr(err) 830 | is.Equal(len(flags), 0) 831 | } 832 | 833 | func TestFlagStringMapEmptyDefaultHelp(t *testing.T) { 834 | is := is.New(t) 835 | actual := new(bytes.Buffer) 836 | called := 0 837 | cli := cli.New("cli", "desc").Writer(actual) 838 | cli.Run(func(ctx context.Context) error { 839 | called++ 840 | return nil 841 | }) 842 | var flags map[string]string 843 | cli.Flag("flag", "cli flag").StringMap(&flags).Default(map[string]string{}) 844 | ctx := context.Background() 845 | err := cli.Parse(ctx, "-h") 846 | is.NoErr(err) 847 | isEqual(t, actual.String(), ` 848 | {bold}Usage:{reset} 849 | {dim}${reset} cli {dim}[flags]{reset} 850 | 851 | {bold}Description:{reset} 852 | desc 853 | 854 | {bold}Flags:{reset} 855 | --flag {dim}cli flag (default:"{}"){reset} 856 | 857 | `) 858 | } 859 | 860 | func TestArgsStringMap(t *testing.T) { 861 | is := is.New(t) 862 | actual := new(bytes.Buffer) 863 | called := 0 864 | cli := cli.New("cli", "desc").Writer(actual) 865 | cli.Run(func(ctx context.Context) error { 866 | called++ 867 | return nil 868 | }) 869 | var args map[string]string 870 | cli.Args("arg", "arg map").StringMap(&args) 871 | // Can have only one arg 872 | ctx := context.Background() 873 | err := cli.Parse(ctx, "a:1 + 1") 874 | is.NoErr(err) 875 | is.Equal(len(args), 1) 876 | is.Equal(args["a"], "1 + 1") 877 | } 878 | 879 | func TestArgsStringMapRequired(t *testing.T) { 880 | is := is.New(t) 881 | actual := new(bytes.Buffer) 882 | called := 0 883 | cli := cli.New("cli", "desc").Writer(actual) 884 | cli.Run(func(ctx context.Context) error { 885 | called++ 886 | return nil 887 | }) 888 | var args map[string]string 889 | cli.Args("arg", "arg map").StringMap(&args) 890 | ctx := context.Background() 891 | err := cli.Parse(ctx) 892 | is.True(err != nil) 893 | is.Equal(err.Error(), "missing ") 894 | } 895 | 896 | func TestArgsStringMapDefault(t *testing.T) { 897 | is := is.New(t) 898 | actual := new(bytes.Buffer) 899 | called := 0 900 | cli := cli.New("cli", "desc").Writer(actual) 901 | cli.Run(func(ctx context.Context) error { 902 | called++ 903 | return nil 904 | }) 905 | var args map[string]string 906 | cli.Args("arg", "arg map").StringMap(&args).Default(map[string]string{ 907 | "a": "1", 908 | "b": "2", 909 | }) 910 | ctx := context.Background() 911 | err := cli.Parse(ctx) 912 | is.NoErr(err) 913 | is.Equal(len(args), 2) 914 | is.Equal(args["a"], "1") 915 | is.Equal(args["b"], "2") 916 | } 917 | 918 | func TestSub(t *testing.T) { 919 | is := is.New(t) 920 | actual := new(bytes.Buffer) 921 | cli := cli.New("bud", "bud CLI").Writer(actual) 922 | var trace []string 923 | cli.Run(func(ctx context.Context) error { 924 | trace = append(trace, "bud") 925 | return nil 926 | }) 927 | { 928 | sub := cli.Command("run", "run your application") 929 | sub.Run(func(ctx context.Context) error { 930 | trace = append(trace, "run") 931 | return nil 932 | }) 933 | } 934 | { 935 | sub := cli.Command("build", "build your application") 936 | sub.Run(func(ctx context.Context) error { 937 | trace = append(trace, "build") 938 | return nil 939 | }) 940 | } 941 | ctx := context.Background() 942 | err := cli.Parse(ctx, "build") 943 | is.NoErr(err) 944 | is.Equal(len(trace), 1) 945 | is.Equal(trace[0], "build") 946 | isEqual(t, actual.String(), ``) 947 | } 948 | 949 | func TestSubHelp(t *testing.T) { 950 | is := is.New(t) 951 | actual := new(bytes.Buffer) 952 | cli := cli.New("bud", "bud CLI").Writer(actual) 953 | cli.Flag("log", "specify the logger").Bool(nil) 954 | cli.Command("run", "run your application") 955 | cli.Command("build", "build your application") 956 | ctx := context.Background() 957 | err := cli.Parse(ctx, "-h") 958 | is.NoErr(err) 959 | isEqual(t, actual.String(), ` 960 | {bold}Usage:{reset} 961 | {dim}${reset} bud {dim}[flags]{reset} {dim}[command]{reset} 962 | 963 | {bold}Description:{reset} 964 | bud CLI 965 | 966 | {bold}Flags:{reset} 967 | --log {dim}specify the logger{reset} 968 | 969 | {bold}Commands:{reset} 970 | build {dim}build your application{reset} 971 | run {dim}run your application{reset} 972 | 973 | `) 974 | } 975 | 976 | func TestEmptyUsage(t *testing.T) { 977 | is := is.New(t) 978 | actual := new(bytes.Buffer) 979 | cli := cli.New("bud", "bud CLI").Writer(actual) 980 | cli.Flag("log", "").Bool(nil) 981 | cli.Command("run", "") 982 | ctx := context.Background() 983 | err := cli.Parse(ctx, "-h") 984 | is.NoErr(err) 985 | isEqual(t, actual.String(), ` 986 | {bold}Usage:{reset} 987 | {dim}${reset} bud {dim}[flags]{reset} {dim}[command]{reset} 988 | 989 | {bold}Description:{reset} 990 | bud CLI 991 | 992 | {bold}Flags:{reset} 993 | --log 994 | 995 | {bold}Commands:{reset} 996 | run 997 | 998 | `) 999 | } 1000 | 1001 | func TestSubHelpShort(t *testing.T) { 1002 | is := is.New(t) 1003 | actual := new(bytes.Buffer) 1004 | cli := cli.New("bud", "bud CLI").Writer(actual) 1005 | cli.Flag("log", "specify the logger").Short('L').Bool(nil).Default(false) 1006 | cli.Flag("debug", "set the debugger").Bool(nil).Default(true) 1007 | var trace []string 1008 | cli.Run(func(ctx context.Context) error { 1009 | trace = append(trace, "bud") 1010 | return nil 1011 | }) 1012 | { 1013 | sub := cli.Command("run", "run your application") 1014 | sub.Run(func(ctx context.Context) error { 1015 | trace = append(trace, "run") 1016 | return nil 1017 | }) 1018 | } 1019 | { 1020 | sub := cli.Command("build", "build your application") 1021 | sub.Run(func(ctx context.Context) error { 1022 | trace = append(trace, "build") 1023 | return nil 1024 | }) 1025 | } 1026 | ctx := context.Background() 1027 | err := cli.Parse(ctx, "-h") 1028 | is.NoErr(err) 1029 | isEqual(t, actual.String(), ` 1030 | {bold}Usage:{reset} 1031 | {dim}${reset} bud {dim}[flags]{reset} {dim}[command]{reset} 1032 | 1033 | {bold}Description:{reset} 1034 | bud CLI 1035 | 1036 | {bold}Flags:{reset} 1037 | -L, --log {dim}specify the logger (default:"false"){reset} 1038 | --debug {dim}set the debugger (default:"true"){reset} 1039 | 1040 | {bold}Commands:{reset} 1041 | build {dim}build your application{reset} 1042 | run {dim}run your application{reset} 1043 | 1044 | `) 1045 | } 1046 | 1047 | func TestArgString(t *testing.T) { 1048 | is := is.New(t) 1049 | actual := new(bytes.Buffer) 1050 | called := 0 1051 | cli := cli.New("cli", "cli command").Writer(actual) 1052 | cli.Run(func(ctx context.Context) error { 1053 | called++ 1054 | return nil 1055 | }) 1056 | var arg string 1057 | cli.Arg("arg", "arg string").String(&arg) 1058 | ctx := context.Background() 1059 | err := cli.Parse(ctx, "cool") 1060 | is.NoErr(err) 1061 | is.Equal(1, called) 1062 | is.Equal(arg, "cool") 1063 | isEqual(t, actual.String(), ``) 1064 | } 1065 | 1066 | func TestArgStringDefault(t *testing.T) { 1067 | is := is.New(t) 1068 | actual := new(bytes.Buffer) 1069 | called := 0 1070 | cli := cli.New("cli", "cli command").Writer(actual) 1071 | cli.Run(func(ctx context.Context) error { 1072 | called++ 1073 | return nil 1074 | }) 1075 | var arg string 1076 | cli.Arg("arg", "arg string").String(&arg).Default("default") 1077 | ctx := context.Background() 1078 | err := cli.Parse(ctx) 1079 | is.NoErr(err) 1080 | is.Equal(1, called) 1081 | is.Equal(arg, "default") 1082 | isEqual(t, actual.String(), ``) 1083 | } 1084 | 1085 | func TestArgStringRequired(t *testing.T) { 1086 | is := is.New(t) 1087 | actual := new(bytes.Buffer) 1088 | called := 0 1089 | cli := cli.New("cli", "cli command").Writer(actual) 1090 | cli.Run(func(ctx context.Context) error { 1091 | called++ 1092 | return nil 1093 | }) 1094 | var arg string 1095 | cli.Arg("arg", "arg string").String(&arg) 1096 | ctx := context.Background() 1097 | err := cli.Parse(ctx) 1098 | is.True(err != nil) 1099 | is.Equal(err.Error(), "missing ") 1100 | } 1101 | 1102 | func TestSubArgString(t *testing.T) { 1103 | is := is.New(t) 1104 | actual := new(bytes.Buffer) 1105 | called := 0 1106 | cli := cli.New("cli", "cli command").Writer(actual) 1107 | cli.Run(func(ctx context.Context) error { 1108 | called++ 1109 | return nil 1110 | }) 1111 | var arg string 1112 | cli.Command("build", "build command") 1113 | cli.Command("run", "run command") 1114 | cli.Arg("arg", "arg string").String(&arg) 1115 | ctx := context.Background() 1116 | err := cli.Parse(ctx, "deploy") 1117 | is.NoErr(err) 1118 | is.Equal(1, called) 1119 | is.Equal(arg, "deploy") 1120 | isEqual(t, actual.String(), ``) 1121 | } 1122 | 1123 | // TestInterrupt tests interrupts canceling context. It spawns a copy of itself 1124 | // to run a subcommand. I learned this trick from Mitchell Hashimoto's excellent 1125 | // "Advanced Testing with Go" talk. We use stdout to synchronize between the 1126 | // process and subprocess. 1127 | func TestInterrupt(t *testing.T) { 1128 | // Start the CLI and cancel it once "ready" is printed. 1129 | parent := func(t testing.TB, cmd *exec.Cmd) { 1130 | is := is.New(t) 1131 | stdout, err := cmd.StdoutPipe() 1132 | is.NoErr(err) 1133 | cmd.Stderr = os.Stderr 1134 | is.NoErr(cmd.Start()) 1135 | scanner := bufio.NewScanner(stdout) 1136 | for scanner.Scan() { 1137 | line := scanner.Text() 1138 | if line == "ready" { 1139 | break 1140 | } 1141 | } 1142 | cmd.Process.Signal(os.Interrupt) 1143 | for scanner.Scan() { 1144 | line := scanner.Text() 1145 | if line == "cancelled" { 1146 | break 1147 | } 1148 | } 1149 | if err := cmd.Wait(); err != nil { 1150 | is.True(errors.Is(err, context.Canceled)) 1151 | } 1152 | } 1153 | 1154 | // The CLI that we run in the subprocess. 1155 | child := func(t testing.TB) { 1156 | is := is.New(t) 1157 | cli := cli.New("cli", "cli command").Trap(os.Interrupt) 1158 | cli.Run(func(ctx context.Context) error { 1159 | os.Stdout.Write([]byte("ready\n")) 1160 | <-ctx.Done() 1161 | os.Stdout.Write([]byte("cancelled\n")) 1162 | return nil 1163 | }) 1164 | ctx := context.Background() 1165 | if err := cli.Parse(ctx); err != nil { 1166 | if errors.Is(err, context.Canceled) { 1167 | return 1168 | } 1169 | is.NoErr(err) 1170 | } 1171 | } 1172 | 1173 | testchild.Run(t, parent, child) 1174 | } 1175 | 1176 | // TODO: example support 1177 | 1178 | func TestArgsStrings(t *testing.T) { 1179 | is := is.New(t) 1180 | actual := new(bytes.Buffer) 1181 | called := 0 1182 | cli := cli.New("cli", "cli command").Writer(actual) 1183 | cli.Run(func(ctx context.Context) error { 1184 | called++ 1185 | return nil 1186 | }) 1187 | var args []string 1188 | cli.Command("build", "build command") 1189 | cli.Command("run", "run command") 1190 | cli.Args("custom", "custom strings").Strings(&args) 1191 | ctx := context.Background() 1192 | err := cli.Parse(ctx, "new", "view") 1193 | is.NoErr(err) 1194 | is.Equal(1, called) 1195 | is.Equal(len(args), 2) 1196 | is.Equal(args[0], "new") 1197 | is.Equal(args[1], "view") 1198 | isEqual(t, actual.String(), ``) 1199 | } 1200 | 1201 | func TestUsageError(t *testing.T) { 1202 | is := is.New(t) 1203 | actual := new(bytes.Buffer) 1204 | called := 0 1205 | cmd := cli.New("cli", "cli command").Writer(actual) 1206 | cmd.Run(func(ctx context.Context) error { 1207 | called++ 1208 | return cli.Usage() 1209 | }) 1210 | ctx := context.Background() 1211 | err := cmd.Parse(ctx) 1212 | is.NoErr(err) 1213 | isEqual(t, actual.String(), ` 1214 | {bold}Usage:{reset} 1215 | {dim}${reset} cli 1216 | 1217 | {bold}Description:{reset} 1218 | cli command 1219 | 1220 | `) 1221 | } 1222 | 1223 | func TestIdempotent(t *testing.T) { 1224 | is := is.New(t) 1225 | actual := new(bytes.Buffer) 1226 | cli := cli.New("cli", "cli command").Writer(actual) 1227 | var f1 string 1228 | cmd := cli.Command("run", "run command") 1229 | cmd.Flag("f1", "cli flag").Short('f').String(&f1) 1230 | var f2 string 1231 | cmd.Flag("f2", "cli flag").String(&f2) 1232 | var f3 string 1233 | cmd.Flag("f3", "cli flag").String(&f3) 1234 | ctx := context.Background() 1235 | args := []string{"run", "--f1=a", "--f2=b", "--f3", "c"} 1236 | err := cli.Parse(ctx, args...) 1237 | is.NoErr(err) 1238 | is.Equal(f1, "a") 1239 | is.Equal(f2, "b") 1240 | is.Equal(f3, "c") 1241 | f1 = "" 1242 | f2 = "" 1243 | f3 = "" 1244 | err = cli.Parse(ctx, args...) 1245 | is.NoErr(err) 1246 | is.Equal(f1, "a") 1247 | is.Equal(f2, "b") 1248 | is.Equal(f3, "c") 1249 | } 1250 | 1251 | func TestManualHelp(t *testing.T) { 1252 | is := is.New(t) 1253 | actual := new(bytes.Buffer) 1254 | cli := cli.New("cli", "cli command").Writer(actual) 1255 | var help bool 1256 | var dir string 1257 | cli.Flag("help", "help menu").Short('h').Bool(&help).Default(false) 1258 | cli.Flag("chdir", "change directory").Short('C').String(&dir) 1259 | called := 0 1260 | cli.Run(func(ctx context.Context) error { 1261 | is.Equal(help, true) 1262 | is.Equal(dir, "somewhere") 1263 | called++ 1264 | return nil 1265 | }) 1266 | ctx := context.Background() 1267 | err := cli.Parse(ctx, "--help", "--chdir", "somewhere") 1268 | is.NoErr(err) 1269 | is.Equal(actual.String(), "") 1270 | is.Equal(called, 1) 1271 | } 1272 | 1273 | func TestManualHelpUsage(t *testing.T) { 1274 | is := is.New(t) 1275 | actual := new(bytes.Buffer) 1276 | cmd := cli.New("cli", "cli command").Writer(actual) 1277 | var help bool 1278 | var dir string 1279 | cmd.Flag("help", "help menu").Short('h').Bool(&help).Default(false) 1280 | cmd.Flag("chdir", "change directory").Short('C').String(&dir) 1281 | called := 0 1282 | cmd.Run(func(ctx context.Context) error { 1283 | is.Equal(help, true) 1284 | is.Equal(dir, "somewhere") 1285 | called++ 1286 | return cli.Usage() 1287 | }) 1288 | ctx := context.Background() 1289 | err := cmd.Parse(ctx, "--help", "--chdir", "somewhere") 1290 | is.NoErr(err) 1291 | is.Equal(called, 1) 1292 | isEqual(t, actual.String(), ` 1293 | {bold}Usage:{reset} 1294 | {dim}${reset} cli {dim}[flags]{reset} 1295 | 1296 | {bold}Description:{reset} 1297 | cli command 1298 | 1299 | {bold}Flags:{reset} 1300 | -C, --chdir {dim}change directory{reset} 1301 | -h, --help {dim}help menu (default:"false"){reset} 1302 | 1303 | `) 1304 | } 1305 | 1306 | func TestAfterRun(t *testing.T) { 1307 | is := is.New(t) 1308 | actual := new(bytes.Buffer) 1309 | cli := cli.New("cli", "cli command").Writer(actual) 1310 | called := 0 1311 | var ctx context.Context 1312 | cli.Run(func(c context.Context) error { 1313 | called++ 1314 | ctx = c 1315 | return nil 1316 | }) 1317 | err := cli.Parse(context.Background()) 1318 | is.NoErr(err) 1319 | is.Equal(called, 1) 1320 | select { 1321 | case <-ctx.Done(): 1322 | is.Fail() // Context shouldn't have been cancelled 1323 | default: 1324 | } 1325 | } 1326 | 1327 | func TestArgsClearSlice(t *testing.T) { 1328 | is := is.New(t) 1329 | actual := new(bytes.Buffer) 1330 | called := 0 1331 | cli := cli.New("cli", "cli command").Writer(actual) 1332 | cli.Run(func(ctx context.Context) error { 1333 | called++ 1334 | return nil 1335 | }) 1336 | args := []string{"a", "b"} 1337 | cli.Args("custom", "custom strings").Strings(&args) 1338 | ctx := context.Background() 1339 | err := cli.Parse(ctx, "c", "d") 1340 | is.NoErr(err) 1341 | is.Equal(1, called) 1342 | is.Equal(len(args), 2) 1343 | is.Equal(args[0], "c") 1344 | is.Equal(args[1], "d") 1345 | isEqual(t, actual.String(), ``) 1346 | } 1347 | 1348 | func TestArgClearMap(t *testing.T) { 1349 | is := is.New(t) 1350 | actual := new(bytes.Buffer) 1351 | called := 0 1352 | cli := cli.New("cli", "cli command").Writer(actual) 1353 | cli.Run(func(ctx context.Context) error { 1354 | called++ 1355 | return nil 1356 | }) 1357 | args := map[string]string{"a": "a"} 1358 | cli.Args("custom", "custom string map").StringMap(&args) 1359 | ctx := context.Background() 1360 | err := cli.Parse(ctx, "b:b") 1361 | is.NoErr(err) 1362 | is.Equal(1, called) 1363 | is.Equal(len(args), 1) 1364 | is.Equal(args["b"], "b") 1365 | isEqual(t, actual.String(), ``) 1366 | } 1367 | 1368 | func TestFlagClearSlice(t *testing.T) { 1369 | is := is.New(t) 1370 | actual := new(bytes.Buffer) 1371 | called := 0 1372 | cli := cli.New("cli", "cli command").Writer(actual) 1373 | cli.Run(func(ctx context.Context) error { 1374 | called++ 1375 | return nil 1376 | }) 1377 | args := []string{"a", "b"} 1378 | cli.Flag("f", "flag").Strings(&args) 1379 | ctx := context.Background() 1380 | err := cli.Parse(ctx, "-f", "c", "-f", "d") 1381 | is.NoErr(err) 1382 | is.Equal(1, called) 1383 | is.Equal(len(args), 2) 1384 | is.Equal(args[0], "c") 1385 | is.Equal(args[1], "d") 1386 | isEqual(t, actual.String(), ``) 1387 | } 1388 | 1389 | func TestFlagClearMap(t *testing.T) { 1390 | is := is.New(t) 1391 | actual := new(bytes.Buffer) 1392 | called := 0 1393 | cli := cli.New("cli", "cli command").Writer(actual) 1394 | cli.Run(func(ctx context.Context) error { 1395 | called++ 1396 | return nil 1397 | }) 1398 | args := map[string]string{"a": "a"} 1399 | cli.Flag("f", "flag").StringMap(&args) 1400 | ctx := context.Background() 1401 | err := cli.Parse(ctx, "-f", "b:b") 1402 | is.NoErr(err) 1403 | is.Equal(1, called) 1404 | is.Equal(len(args), 1) 1405 | is.Equal(args["b"], "b") 1406 | isEqual(t, actual.String(), ``) 1407 | } 1408 | 1409 | func TestFlagOptionalString(t *testing.T) { 1410 | is := is.New(t) 1411 | actual := new(bytes.Buffer) 1412 | called := 0 1413 | cli := cli.New("cli", "cli command").Writer(actual) 1414 | var s *string 1415 | cli.Flag("s", "string").Optional().String(&s) 1416 | cli.Run(func(ctx context.Context) error { 1417 | called++ 1418 | return nil 1419 | }) 1420 | ctx := context.Background() 1421 | err := cli.Parse(ctx, "--s", "foo") 1422 | is.NoErr(err) 1423 | is.Equal(1, called) 1424 | is.Equal(*s, "foo") 1425 | } 1426 | 1427 | func TestFlagOptionalStringDefault(t *testing.T) { 1428 | is := is.New(t) 1429 | actual := new(bytes.Buffer) 1430 | called := 0 1431 | cli := cli.New("cli", "cli command").Writer(actual) 1432 | var s *string 1433 | cli.Flag("s", "string").Optional().String(&s).Default("foo") 1434 | cli.Run(func(ctx context.Context) error { 1435 | called++ 1436 | return nil 1437 | }) 1438 | ctx := context.Background() 1439 | err := cli.Parse(ctx) 1440 | is.NoErr(err) 1441 | is.Equal(1, called) 1442 | is.Equal(*s, "foo") 1443 | } 1444 | 1445 | func TestFlagOptionalStringNil(t *testing.T) { 1446 | is := is.New(t) 1447 | actual := new(bytes.Buffer) 1448 | called := 0 1449 | cli := cli.New("cli", "cli command").Writer(actual) 1450 | var s *string 1451 | cli.Flag("s", "string").Optional().String(&s) 1452 | cli.Run(func(ctx context.Context) error { 1453 | called++ 1454 | return nil 1455 | }) 1456 | ctx := context.Background() 1457 | err := cli.Parse(ctx) 1458 | is.NoErr(err) 1459 | is.Equal(1, called) 1460 | is.True(s == nil) 1461 | } 1462 | 1463 | func TestFlagOptionalBoolTrue(t *testing.T) { 1464 | is := is.New(t) 1465 | actual := new(bytes.Buffer) 1466 | called := 0 1467 | cli := cli.New("cli", "cli command").Writer(actual) 1468 | var b *bool 1469 | cli.Flag("b", "bool").Optional().Bool(&b) 1470 | cli.Run(func(ctx context.Context) error { 1471 | called++ 1472 | return nil 1473 | }) 1474 | ctx := context.Background() 1475 | err := cli.Parse(ctx, "--b") 1476 | is.NoErr(err) 1477 | is.Equal(1, called) 1478 | is.True(*b) 1479 | } 1480 | 1481 | func TestFlagOptionalBoolFalse(t *testing.T) { 1482 | is := is.New(t) 1483 | actual := new(bytes.Buffer) 1484 | called := 0 1485 | cli := cli.New("cli", "cli command").Writer(actual) 1486 | var b *bool 1487 | cli.Flag("b", "bool").Optional().Bool(&b) 1488 | cli.Run(func(ctx context.Context) error { 1489 | called++ 1490 | return nil 1491 | }) 1492 | ctx := context.Background() 1493 | err := cli.Parse(ctx, "--b=false") 1494 | is.NoErr(err) 1495 | is.Equal(1, called) 1496 | is.True(!*b) 1497 | } 1498 | 1499 | func TestFlagOptionalBoolDefault(t *testing.T) { 1500 | is := is.New(t) 1501 | actual := new(bytes.Buffer) 1502 | called := 0 1503 | cli := cli.New("cli", "cli command").Writer(actual) 1504 | var b *bool 1505 | cli.Flag("b", "bool").Optional().Bool(&b).Default(true) 1506 | cli.Run(func(ctx context.Context) error { 1507 | called++ 1508 | return nil 1509 | }) 1510 | ctx := context.Background() 1511 | err := cli.Parse(ctx) 1512 | is.NoErr(err) 1513 | is.Equal(1, called) 1514 | is.True(*b) 1515 | } 1516 | 1517 | func TestFlagOptionalBoolNil(t *testing.T) { 1518 | is := is.New(t) 1519 | actual := new(bytes.Buffer) 1520 | called := 0 1521 | cli := cli.New("cli", "cli command").Writer(actual) 1522 | var b *bool 1523 | cli.Flag("b", "bool").Optional().Bool(&b) 1524 | cli.Run(func(ctx context.Context) error { 1525 | called++ 1526 | return nil 1527 | }) 1528 | ctx := context.Background() 1529 | err := cli.Parse(ctx) 1530 | is.NoErr(err) 1531 | is.Equal(1, called) 1532 | is.Equal(b, nil) 1533 | } 1534 | 1535 | func TestFlagOptionalInt(t *testing.T) { 1536 | is := is.New(t) 1537 | actual := new(bytes.Buffer) 1538 | called := 0 1539 | cli := cli.New("cli", "cli command").Writer(actual) 1540 | var i *int 1541 | cli.Flag("i", "int").Optional().Int(&i) 1542 | cli.Run(func(ctx context.Context) error { 1543 | called++ 1544 | return nil 1545 | }) 1546 | ctx := context.Background() 1547 | err := cli.Parse(ctx, "--i=1") 1548 | is.NoErr(err) 1549 | is.Equal(1, called) 1550 | is.Equal(*i, 1) 1551 | } 1552 | 1553 | func TestFlagOptionalIntDefault(t *testing.T) { 1554 | is := is.New(t) 1555 | actual := new(bytes.Buffer) 1556 | called := 0 1557 | cli := cli.New("cli", "cli command").Writer(actual) 1558 | var i *int 1559 | cli.Flag("i", "int").Optional().Int(&i).Default(1) 1560 | cli.Run(func(ctx context.Context) error { 1561 | called++ 1562 | return nil 1563 | }) 1564 | ctx := context.Background() 1565 | err := cli.Parse(ctx) 1566 | is.NoErr(err) 1567 | is.Equal(1, called) 1568 | is.Equal(*i, 1) 1569 | } 1570 | 1571 | func TestFlagOptionalIntNil(t *testing.T) { 1572 | is := is.New(t) 1573 | actual := new(bytes.Buffer) 1574 | called := 0 1575 | cli := cli.New("cli", "cli command").Writer(actual) 1576 | var i *int 1577 | cli.Flag("i", "int").Optional().Int(&i) 1578 | cli.Run(func(ctx context.Context) error { 1579 | called++ 1580 | return nil 1581 | }) 1582 | ctx := context.Background() 1583 | err := cli.Parse(ctx) 1584 | is.NoErr(err) 1585 | is.Equal(1, called) 1586 | is.Equal(i, nil) 1587 | } 1588 | 1589 | func TestArgOptionalString(t *testing.T) { 1590 | is := is.New(t) 1591 | actual := new(bytes.Buffer) 1592 | called := 0 1593 | cli := cli.New("cli", "cli command").Writer(actual) 1594 | var s *string 1595 | cli.Arg("s", "string arg").Optional().String(&s) 1596 | cli.Run(func(ctx context.Context) error { 1597 | called++ 1598 | return nil 1599 | }) 1600 | ctx := context.Background() 1601 | err := cli.Parse(ctx, "foo") 1602 | is.NoErr(err) 1603 | is.Equal(1, called) 1604 | is.Equal(*s, "foo") 1605 | } 1606 | 1607 | func TestArgOptionalStringDefault(t *testing.T) { 1608 | is := is.New(t) 1609 | actual := new(bytes.Buffer) 1610 | called := 0 1611 | cli := cli.New("cli", "cli command").Writer(actual) 1612 | var s *string 1613 | cli.Arg("s", "string arg").Optional().String(&s).Default("foo") 1614 | cli.Run(func(ctx context.Context) error { 1615 | called++ 1616 | return nil 1617 | }) 1618 | ctx := context.Background() 1619 | err := cli.Parse(ctx) 1620 | is.NoErr(err) 1621 | is.Equal(1, called) 1622 | is.Equal(*s, "foo") 1623 | } 1624 | 1625 | func TestArgOptionalStringNil(t *testing.T) { 1626 | is := is.New(t) 1627 | actual := new(bytes.Buffer) 1628 | called := 0 1629 | cli := cli.New("cli", "cli command").Writer(actual) 1630 | var s *string 1631 | cli.Arg("s", "string arg").Optional().String(&s) 1632 | cli.Run(func(ctx context.Context) error { 1633 | called++ 1634 | return nil 1635 | }) 1636 | ctx := context.Background() 1637 | err := cli.Parse(ctx) 1638 | is.NoErr(err) 1639 | is.Equal(1, called) 1640 | is.True(s == nil) 1641 | } 1642 | 1643 | func TestArgOptionalBoolTrue(t *testing.T) { 1644 | is := is.New(t) 1645 | actual := new(bytes.Buffer) 1646 | called := 0 1647 | cli := cli.New("cli", "cli command").Writer(actual) 1648 | var b *bool 1649 | cli.Arg("b", "bool arg").Optional().Bool(&b) 1650 | cli.Run(func(ctx context.Context) error { 1651 | called++ 1652 | return nil 1653 | }) 1654 | ctx := context.Background() 1655 | err := cli.Parse(ctx, "true") 1656 | is.NoErr(err) 1657 | is.Equal(1, called) 1658 | is.True(*b) 1659 | } 1660 | 1661 | func TestArgOptionalBoolFalse(t *testing.T) { 1662 | is := is.New(t) 1663 | actual := new(bytes.Buffer) 1664 | called := 0 1665 | cli := cli.New("cli", "cli command").Writer(actual) 1666 | var b *bool 1667 | cli.Arg("b", "bool arg").Optional().Bool(&b) 1668 | cli.Run(func(ctx context.Context) error { 1669 | called++ 1670 | return nil 1671 | }) 1672 | ctx := context.Background() 1673 | err := cli.Parse(ctx, "false") 1674 | is.NoErr(err) 1675 | is.Equal(1, called) 1676 | is.True(!*b) 1677 | } 1678 | 1679 | func TestArgOptionalBoolDefault(t *testing.T) { 1680 | is := is.New(t) 1681 | actual := new(bytes.Buffer) 1682 | called := 0 1683 | cli := cli.New("cli", "cli command").Writer(actual) 1684 | var b *bool 1685 | cli.Arg("b", "bool arg").Optional().Bool(&b).Default(true) 1686 | cli.Run(func(ctx context.Context) error { 1687 | called++ 1688 | return nil 1689 | }) 1690 | ctx := context.Background() 1691 | err := cli.Parse(ctx) 1692 | is.NoErr(err) 1693 | is.Equal(1, called) 1694 | is.True(*b) 1695 | } 1696 | 1697 | func TestArgOptionalBoolNil(t *testing.T) { 1698 | is := is.New(t) 1699 | actual := new(bytes.Buffer) 1700 | called := 0 1701 | cli := cli.New("cli", "cli command").Writer(actual) 1702 | var b *bool 1703 | cli.Arg("b", "bool arg").Optional().Bool(&b) 1704 | cli.Run(func(ctx context.Context) error { 1705 | called++ 1706 | return nil 1707 | }) 1708 | ctx := context.Background() 1709 | err := cli.Parse(ctx) 1710 | is.NoErr(err) 1711 | is.Equal(1, called) 1712 | is.Equal(b, nil) 1713 | } 1714 | 1715 | func TestArgOptionalInt(t *testing.T) { 1716 | is := is.New(t) 1717 | actual := new(bytes.Buffer) 1718 | called := 0 1719 | cli := cli.New("cli", "cli command").Writer(actual) 1720 | var i *int 1721 | cli.Arg("i", "int arg").Optional().Int(&i) 1722 | cli.Run(func(ctx context.Context) error { 1723 | called++ 1724 | return nil 1725 | }) 1726 | ctx := context.Background() 1727 | err := cli.Parse(ctx, "1") 1728 | is.NoErr(err) 1729 | is.Equal(1, called) 1730 | is.Equal(*i, 1) 1731 | } 1732 | 1733 | func TestArgOptionalIntDefault(t *testing.T) { 1734 | is := is.New(t) 1735 | actual := new(bytes.Buffer) 1736 | called := 0 1737 | cli := cli.New("cli", "cli command").Writer(actual) 1738 | var i *int 1739 | cli.Arg("i", "int arg").Optional().Int(&i).Default(1) 1740 | cli.Run(func(ctx context.Context) error { 1741 | called++ 1742 | return nil 1743 | }) 1744 | ctx := context.Background() 1745 | err := cli.Parse(ctx) 1746 | is.NoErr(err) 1747 | is.Equal(1, called) 1748 | is.Equal(*i, 1) 1749 | } 1750 | 1751 | func TestArgOptionalIntNil(t *testing.T) { 1752 | is := is.New(t) 1753 | actual := new(bytes.Buffer) 1754 | called := 0 1755 | cli := cli.New("cli", "cli command").Writer(actual) 1756 | var i *int 1757 | cli.Arg("i", "int arg").Optional().Int(&i) 1758 | cli.Run(func(ctx context.Context) error { 1759 | called++ 1760 | return nil 1761 | }) 1762 | ctx := context.Background() 1763 | err := cli.Parse(ctx) 1764 | is.NoErr(err) 1765 | is.Equal(1, called) 1766 | is.Equal(i, nil) 1767 | } 1768 | 1769 | func TestFlagOptionalStrings(t *testing.T) { 1770 | is := is.New(t) 1771 | actual := new(bytes.Buffer) 1772 | called := 0 1773 | cli := cli.New("cli", "cli command").Writer(actual) 1774 | var s []string 1775 | cli.Flag("s", "s").Optional().Strings(&s) 1776 | cli.Run(func(ctx context.Context) error { 1777 | called++ 1778 | return nil 1779 | }) 1780 | ctx := context.Background() 1781 | err := cli.Parse(ctx, "--s=foo", "--s=bar") 1782 | is.NoErr(err) 1783 | is.Equal(1, called) 1784 | is.Equal(s, []string{"foo", "bar"}) 1785 | } 1786 | 1787 | func TestFlagOptionalStringsDefault(t *testing.T) { 1788 | is := is.New(t) 1789 | actual := new(bytes.Buffer) 1790 | called := 0 1791 | cli := cli.New("cli", "cli command").Writer(actual) 1792 | var s []string 1793 | cli.Flag("s", "s").Optional().Strings(&s).Default("foo", "bar") 1794 | cli.Run(func(ctx context.Context) error { 1795 | called++ 1796 | return nil 1797 | }) 1798 | ctx := context.Background() 1799 | err := cli.Parse(ctx) 1800 | is.NoErr(err) 1801 | is.Equal(1, called) 1802 | is.Equal(s, []string{"foo", "bar"}) 1803 | } 1804 | 1805 | func TestFlagOptionalStringsEmpty(t *testing.T) { 1806 | is := is.New(t) 1807 | actual := new(bytes.Buffer) 1808 | called := 0 1809 | cli := cli.New("cli", "cli command").Writer(actual) 1810 | var s []string 1811 | cli.Flag("s", "s").Optional().Strings(&s) 1812 | cli.Run(func(ctx context.Context) error { 1813 | called++ 1814 | return nil 1815 | }) 1816 | ctx := context.Background() 1817 | err := cli.Parse(ctx) 1818 | is.NoErr(err) 1819 | is.Equal(1, called) 1820 | is.Equal(s, nil) 1821 | } 1822 | 1823 | func TestArgsOptionalStrings(t *testing.T) { 1824 | is := is.New(t) 1825 | actual := new(bytes.Buffer) 1826 | called := 0 1827 | cli := cli.New("cli", "cli command").Writer(actual) 1828 | var s []string 1829 | cli.Args("s", "strings args").Optional().Strings(&s) 1830 | cli.Run(func(ctx context.Context) error { 1831 | called++ 1832 | return nil 1833 | }) 1834 | ctx := context.Background() 1835 | err := cli.Parse(ctx, "foo", "bar") 1836 | is.NoErr(err) 1837 | is.Equal(1, called) 1838 | is.Equal(s, []string{"foo", "bar"}) 1839 | } 1840 | 1841 | func TestArgsOptionalStringsDefault(t *testing.T) { 1842 | is := is.New(t) 1843 | actual := new(bytes.Buffer) 1844 | called := 0 1845 | cli := cli.New("cli", "cli command").Writer(actual) 1846 | var s []string 1847 | cli.Args("s", "strings args").Optional().Strings(&s).Default("foo", "bar") 1848 | cli.Run(func(ctx context.Context) error { 1849 | called++ 1850 | return nil 1851 | }) 1852 | ctx := context.Background() 1853 | err := cli.Parse(ctx) 1854 | is.NoErr(err) 1855 | is.Equal(1, called) 1856 | is.Equal(s, []string{"foo", "bar"}) 1857 | } 1858 | 1859 | func TestArgsOptionalStringsEmpty(t *testing.T) { 1860 | is := is.New(t) 1861 | actual := new(bytes.Buffer) 1862 | called := 0 1863 | cli := cli.New("cli", "cli command").Writer(actual) 1864 | var s []string 1865 | cli.Args("s", "strings args").Optional().Strings(&s) 1866 | cli.Run(func(ctx context.Context) error { 1867 | called++ 1868 | return nil 1869 | }) 1870 | ctx := context.Background() 1871 | err := cli.Parse(ctx) 1872 | is.NoErr(err) 1873 | is.Equal(1, called) 1874 | is.Equal(s, nil) 1875 | } 1876 | 1877 | func TestArgsRequiredStringsMissing(t *testing.T) { 1878 | is := is.New(t) 1879 | actual := new(bytes.Buffer) 1880 | called := 0 1881 | cli := cli.New("cli", "cli command").Writer(actual) 1882 | var s []string 1883 | cli.Args("strings", "strings args").Strings(&s) 1884 | cli.Run(func(ctx context.Context) error { 1885 | called++ 1886 | return nil 1887 | }) 1888 | ctx := context.Background() 1889 | err := cli.Parse(ctx) 1890 | is.True(err != nil) 1891 | is.True(err != nil) 1892 | is.Equal(err.Error(), "missing ") 1893 | is.Equal(0, called) 1894 | } 1895 | 1896 | func TestUsageNestCommandArg(t *testing.T) { 1897 | is := is.New(t) 1898 | actual := new(bytes.Buffer) 1899 | var path string 1900 | called := 0 1901 | cli := cli.New("bud", "bud cli").Writer(actual) 1902 | { 1903 | cli := cli.Command("fs", "filesystem tools") 1904 | { 1905 | cli := cli.Command("cat", "cat a file") 1906 | cli.Arg("path", "path string").String(&path) 1907 | cli.Run(func(ctx context.Context) error { 1908 | called++ 1909 | return nil 1910 | }) 1911 | } 1912 | } 1913 | ctx := context.Background() 1914 | err := cli.Parse(ctx, "fs", "cat", "-h") 1915 | is.NoErr(err) 1916 | isEqual(t, actual.String(), ` 1917 | {bold}Usage:{reset} 1918 | {dim}${reset} bud fs cat {dim}{reset} 1919 | 1920 | {bold}Description:{reset} 1921 | cat a file 1922 | 1923 | {bold}Args:{reset} 1924 | {dim}path string{reset} 1925 | 1926 | `) 1927 | } 1928 | 1929 | func TestHiddenCommand(t *testing.T) { 1930 | is := is.New(t) 1931 | actual := new(bytes.Buffer) 1932 | cli := cli.New("cli", "cli command").Writer(actual) 1933 | cli.Command("foo", "foo command").Hidden() 1934 | cli.Command("bar", "bar command") 1935 | ctx := context.Background() 1936 | err := cli.Parse(ctx, "-h") 1937 | is.NoErr(err) 1938 | isEqual(t, actual.String(), ` 1939 | {bold}Usage:{reset} 1940 | {dim}${reset} cli {dim}[command]{reset} 1941 | 1942 | {bold}Description:{reset} 1943 | cli command 1944 | 1945 | {bold}Commands:{reset} 1946 | bar {dim}bar command{reset} 1947 | 1948 | `) 1949 | } 1950 | 1951 | func TestHiddenCommandRunnable(t *testing.T) { 1952 | is := is.New(t) 1953 | actual := new(bytes.Buffer) 1954 | called := 0 1955 | cli := cli.New("cli", "cli command").Writer(actual) 1956 | cmd := cli.Command("foo", "foo command").Hidden() 1957 | cmd.Run(func(ctx context.Context) error { 1958 | called++ 1959 | return nil 1960 | }) 1961 | ctx := context.Background() 1962 | err := cli.Parse(ctx, "foo") 1963 | is.NoErr(err) 1964 | is.Equal(1, called) 1965 | } 1966 | 1967 | func TestAdvancedCommand(t *testing.T) { 1968 | is := is.New(t) 1969 | actual := new(bytes.Buffer) 1970 | cli := cli.New("cli", "cli command").Writer(actual) 1971 | cli.Command("foo", "foo command") 1972 | cli.Command("bar", "bar command").Advanced() 1973 | ctx := context.Background() 1974 | err := cli.Parse(ctx, "-h") 1975 | is.NoErr(err) 1976 | isEqual(t, actual.String(), ` 1977 | {bold}Usage:{reset} 1978 | {dim}${reset} cli {dim}[command]{reset} 1979 | 1980 | {bold}Description:{reset} 1981 | cli command 1982 | 1983 | {bold}Commands:{reset} 1984 | foo {dim}foo command{reset} 1985 | 1986 | {bold}Advanced Commands:{reset} 1987 | bar {dim}bar command{reset} 1988 | 1989 | `) 1990 | } 1991 | 1992 | type appCmd struct { 1993 | Chdir string 1994 | Embed bool 1995 | } 1996 | 1997 | type newCmd struct { 1998 | Dir string 1999 | Minify bool 2000 | } 2001 | 2002 | // Run new 2003 | func (c *newCmd) Run(ctx context.Context) error { 2004 | os.Stdout.WriteString("running new on " + c.Dir) 2005 | return nil 2006 | } 2007 | 2008 | func ExampleCLI() { 2009 | cmd := &appCmd{} 2010 | cli := cli.New("app", "your awesome cli").Writer(os.Stderr) 2011 | cli.Flag("chdir", "change the dir").Short('C').String(&cmd.Chdir).Default(".") 2012 | cli.Flag("embed", "embed the code").Bool(&cmd.Embed).Default(false) 2013 | 2014 | { // new 2015 | cmd := &newCmd{} 2016 | cli := cli.Command("new", "create a new project") 2017 | cli.Arg("dir", "directory to scaffold in").String(&cmd.Dir) 2018 | cli.Run(cmd.Run) 2019 | } 2020 | 2021 | ctx := context.Background() 2022 | cli.Parse(ctx, "new", ".") 2023 | // Output: 2024 | // running new on . 2025 | } 2026 | 2027 | func TestFlagsAnywhere(t *testing.T) { 2028 | is := is.New(t) 2029 | actual := new(bytes.Buffer) 2030 | var dir string 2031 | var src string 2032 | var path string 2033 | called := 0 2034 | cli := cli.New("bud", "bud cli").Writer(actual) 2035 | cli.Flag("chdir", "change the dir").Short('C').String(&dir).Default(".") 2036 | { 2037 | cli := cli.Command("fs", "filesystem tools") 2038 | cli.Flag("src", "source directory").String(&src) 2039 | { 2040 | cli := cli.Command("cat", "cat a file") 2041 | cli.Flag("path", "path to file").String(&path) 2042 | cli.Run(func(ctx context.Context) error { 2043 | called++ 2044 | return nil 2045 | }) 2046 | } 2047 | } 2048 | ctx := context.Background() 2049 | err := cli.Parse(ctx, "fs", "cat", "-C", "cool", "--src", "http://url.com", "--path", "mypath") 2050 | is.NoErr(err) 2051 | is.Equal(1, called) 2052 | is.Equal(dir, "cool") 2053 | is.Equal(src, "http://url.com") 2054 | is.Equal(path, "mypath") 2055 | } 2056 | 2057 | func TestUnexpectedArg(t *testing.T) { 2058 | is := is.New(t) 2059 | actual := new(bytes.Buffer) 2060 | var dir string 2061 | var src string 2062 | var path string 2063 | called := 0 2064 | cmd := cli.New("bud", "bud cli").Writer(actual) 2065 | cmd.Flag("chdir", "change the dir").Short('C').String(&dir).Default(".") 2066 | { 2067 | cmd := cmd.Command("fs", "filesystem tools") 2068 | cmd.Flag("src", "source directory").String(&src) 2069 | { 2070 | cmd := cmd.Command("cat", "cat a file") 2071 | cmd.Flag("path", "path to file").String(&path) 2072 | cmd.Run(func(ctx context.Context) error { 2073 | called++ 2074 | return nil 2075 | }) 2076 | } 2077 | } 2078 | ctx := context.Background() 2079 | err := cmd.Parse(ctx, "fs:cat", "-C", "cool", "--src", "http://url.com", "--path", "mypath") 2080 | is.True(err != nil) 2081 | is.True(errors.Is(err, cli.ErrInvalidInput)) 2082 | is.Equal(err.Error(), `cli: invalid input with unxpected arg "fs:cat"`) 2083 | } 2084 | 2085 | func TestFlagEnum(t *testing.T) { 2086 | is := is.New(t) 2087 | actual := new(bytes.Buffer) 2088 | called := 0 2089 | cli := cli.New("cli", "desc").Writer(actual) 2090 | cli.Run(func(ctx context.Context) error { 2091 | called++ 2092 | return nil 2093 | }) 2094 | var flag string 2095 | cli.Flag("flag", "cli flag").Enum(&flag, "a", "b", "c") 2096 | ctx := context.Background() 2097 | err := cli.Parse(ctx, "--flag", "a") 2098 | is.NoErr(err) 2099 | is.Equal(1, called) 2100 | is.Equal(flag, "a") 2101 | isEqual(t, actual.String(), ``) 2102 | } 2103 | 2104 | func TestFlagEnumDefault(t *testing.T) { 2105 | is := is.New(t) 2106 | actual := new(bytes.Buffer) 2107 | called := 0 2108 | cli := cli.New("cli", "desc").Writer(actual) 2109 | cli.Run(func(ctx context.Context) error { 2110 | called++ 2111 | return nil 2112 | }) 2113 | var flag string 2114 | cli.Flag("flag", "cli flag").Enum(&flag, "a", "b", "c").Default("b") 2115 | ctx := context.Background() 2116 | err := cli.Parse(ctx) 2117 | is.NoErr(err) 2118 | is.Equal(1, called) 2119 | is.Equal(flag, "b") 2120 | isEqual(t, actual.String(), ``) 2121 | } 2122 | 2123 | func TestFlagEnumRequired(t *testing.T) { 2124 | is := is.New(t) 2125 | actual := new(bytes.Buffer) 2126 | called := 0 2127 | cli := cli.New("cli", "desc").Writer(actual) 2128 | cli.Run(func(ctx context.Context) error { 2129 | called++ 2130 | return nil 2131 | }) 2132 | var flag string 2133 | cli.Flag("flag", "cli flag").Enum(&flag) 2134 | ctx := context.Background() 2135 | err := cli.Parse(ctx) 2136 | is.True(err != nil) 2137 | is.Equal(err.Error(), "missing --flag") 2138 | } 2139 | 2140 | func TestFlagEnumInvalid(t *testing.T) { 2141 | is := is.New(t) 2142 | actual := new(bytes.Buffer) 2143 | called := 0 2144 | cli := cli.New("cli", "desc").Writer(actual) 2145 | cli.Run(func(ctx context.Context) error { 2146 | called++ 2147 | return nil 2148 | }) 2149 | var flag string 2150 | cli.Flag("flag", "cli flag").Enum(&flag, "a", "b", "c") 2151 | ctx := context.Background() 2152 | err := cli.Parse(ctx, "--flag", "d") 2153 | is.True(err != nil) 2154 | is.Equal(err.Error(), `--flag "d" must be either "a", "b" or "c"`) 2155 | } 2156 | 2157 | func TestArgEnum(t *testing.T) { 2158 | is := is.New(t) 2159 | actual := new(bytes.Buffer) 2160 | called := 0 2161 | cli := cli.New("cli", "desc").Writer(actual) 2162 | cli.Run(func(ctx context.Context) error { 2163 | called++ 2164 | return nil 2165 | }) 2166 | var arg string 2167 | cli.Arg("arg", "enum arg").Enum(&arg, "a", "b", "c") 2168 | ctx := context.Background() 2169 | err := cli.Parse(ctx, "a") 2170 | is.NoErr(err) 2171 | is.Equal(1, called) 2172 | is.Equal(arg, "a") 2173 | isEqual(t, actual.String(), ``) 2174 | } 2175 | 2176 | func TestArgEnumDefault(t *testing.T) { 2177 | is := is.New(t) 2178 | actual := new(bytes.Buffer) 2179 | called := 0 2180 | cli := cli.New("cli", "desc").Writer(actual) 2181 | cli.Run(func(ctx context.Context) error { 2182 | called++ 2183 | return nil 2184 | }) 2185 | var arg string 2186 | cli.Arg("arg", "enum arg").Enum(&arg, "a", "b", "c").Default("b") 2187 | ctx := context.Background() 2188 | err := cli.Parse(ctx) 2189 | is.NoErr(err) 2190 | is.Equal(1, called) 2191 | is.Equal(arg, "b") 2192 | isEqual(t, actual.String(), ``) 2193 | } 2194 | 2195 | func TestArgEnumRequired(t *testing.T) { 2196 | is := is.New(t) 2197 | actual := new(bytes.Buffer) 2198 | called := 0 2199 | cli := cli.New("cli", "desc").Writer(actual) 2200 | cli.Run(func(ctx context.Context) error { 2201 | called++ 2202 | return nil 2203 | }) 2204 | var arg string 2205 | cli.Arg("arg", "enum arg").Enum(&arg, "a", "b", "c") 2206 | ctx := context.Background() 2207 | err := cli.Parse(ctx) 2208 | is.True(err != nil) 2209 | is.Equal(err.Error(), "missing ") 2210 | } 2211 | 2212 | func TestArgEnumInvalid(t *testing.T) { 2213 | is := is.New(t) 2214 | actual := new(bytes.Buffer) 2215 | called := 0 2216 | cli := cli.New("cli", "desc").Writer(actual) 2217 | cli.Run(func(ctx context.Context) error { 2218 | called++ 2219 | return nil 2220 | }) 2221 | var arg string 2222 | cli.Arg("arg", "enum arg").Enum(&arg, "a", "b", "c") 2223 | ctx := context.Background() 2224 | err := cli.Parse(ctx, "d") 2225 | is.True(err != nil) 2226 | is.Equal(err.Error(), ` "d" must be either "a", "b" or "c"`) 2227 | } 2228 | 2229 | func TestArgOptionalEnumInvalid(t *testing.T) { 2230 | is := is.New(t) 2231 | actual := new(bytes.Buffer) 2232 | called := 0 2233 | cli := cli.New("cli", "desc").Writer(actual) 2234 | cli.Run(func(ctx context.Context) error { 2235 | called++ 2236 | return nil 2237 | }) 2238 | var arg *string 2239 | cli.Arg("arg", "enum arg").Optional().Enum(&arg, "a", "b", "c") 2240 | ctx := context.Background() 2241 | err := cli.Parse(ctx, "d") 2242 | is.True(err != nil) 2243 | is.Equal(err.Error(), ` "d" must be either "a", "b" or "c"`) 2244 | } 2245 | 2246 | func TestColonBased(t *testing.T) { 2247 | is := is.New(t) 2248 | actual := new(bytes.Buffer) 2249 | var dir string 2250 | var src string 2251 | var path string 2252 | called := 0 2253 | cli := cli.New("bud", "bud cli").Writer(actual) 2254 | cli.Flag("chdir", "change the dir").Short('C').String(&dir).Default(".") 2255 | { 2256 | cli := cli.Command("fs:cat", "cat a file") 2257 | cli.Flag("src", "source directory").String(&src) 2258 | cli.Flag("path", "path to file").String(&path) 2259 | cli.Run(func(ctx context.Context) error { 2260 | called++ 2261 | return nil 2262 | }) 2263 | } 2264 | { 2265 | cli := cli.Command("fs:list", "list a directory") 2266 | cli.Flag("src", "source directory").String(&src) 2267 | cli.Flag("path", "path to directory").String(&path) 2268 | cli.Run(func(ctx context.Context) error { 2269 | called++ 2270 | return nil 2271 | }) 2272 | } 2273 | ctx := context.Background() 2274 | err := cli.Parse(ctx, "-C", "cool", "fs:cat", "--src=http://url.com", "--path", "mypath") 2275 | is.NoErr(err) 2276 | is.Equal(1, called) 2277 | is.Equal(dir, "cool") 2278 | is.Equal(src, "http://url.com") 2279 | is.Equal(path, "mypath") 2280 | err = cli.Parse(ctx, "-C", "cool", "fs:list", "--src=http://url.com", "--path", "mypath") 2281 | is.NoErr(err) 2282 | is.Equal(2, called) 2283 | is.Equal(dir, "cool") 2284 | is.Equal(src, "http://url.com") 2285 | is.Equal(path, "mypath") 2286 | } 2287 | 2288 | func TestFindAndChange(t *testing.T) { 2289 | is := is.New(t) 2290 | actual := new(bytes.Buffer) 2291 | cli := cli.New("cli", "desc").Writer(actual) 2292 | called := []string{} 2293 | cmd := cli.Command("a", "a command") 2294 | cmd.Run(func(ctx context.Context) error { 2295 | called = append(called, "a") 2296 | return nil 2297 | }) 2298 | cmd = cmd.Command("b", "b command") 2299 | cmd.Run(func(ctx context.Context) error { 2300 | called = append(called, "b1") 2301 | return nil 2302 | }) 2303 | cmd, err := cli.Find("a") 2304 | is.NoErr(err) 2305 | cmd.Run(func(ctx context.Context) error { 2306 | called = append(called, "a2") 2307 | return nil 2308 | }) 2309 | 2310 | // Change a 2311 | cmd, err = cli.Find("a") 2312 | is.NoErr(err) 2313 | cmd.Run(func(ctx context.Context) error { 2314 | called = append(called, "a2") 2315 | return nil 2316 | }) 2317 | ctx := context.Background() 2318 | called = []string{} 2319 | err = cli.Parse(ctx, "a") 2320 | is.NoErr(err) 2321 | is.Equal(called, []string{"a2"}) 2322 | 2323 | // Change a b 2324 | cmd, err = cli.Find("a", "b") 2325 | is.NoErr(err) 2326 | cmd.Run(func(ctx context.Context) error { 2327 | called = append(called, "b2") 2328 | return nil 2329 | }) 2330 | called = []string{} 2331 | err = cli.Parse(ctx, "a", "b") 2332 | is.NoErr(err) 2333 | is.Equal(called, []string{"b2"}) 2334 | } 2335 | 2336 | func TestFindNotFound(t *testing.T) { 2337 | is := is.New(t) 2338 | actual := new(bytes.Buffer) 2339 | app := cli.New("cli", "desc").Writer(actual) 2340 | called := []string{} 2341 | cmd := app.Command("a", "a command") 2342 | cmd.Run(func(ctx context.Context) error { 2343 | called = append(called, "a") 2344 | return nil 2345 | }) 2346 | cmd = app.Command("b", "b command") 2347 | cmd.Run(func(ctx context.Context) error { 2348 | called = append(called, "b1") 2349 | return nil 2350 | }) 2351 | cmd, err := app.Find("a", "c") 2352 | is.True(errors.Is(err, cli.ErrCommandNotFound)) 2353 | is.Equal(cmd, nil) 2354 | } 2355 | 2356 | func TestOutOfOrderFlags(t *testing.T) { 2357 | is := is.New(t) 2358 | actual := new(bytes.Buffer) 2359 | var dir string 2360 | var src string 2361 | var path string 2362 | var sync bool 2363 | called := 0 2364 | cli := cli.New("bud", "bud cli").Writer(actual) 2365 | cli.Flag("chdir", "change the dir").Short('C').String(&dir).Default(".") 2366 | { 2367 | cli := cli.Command("fs:cat", "cat a file") 2368 | cli.Flag("src", "source directory").String(&src) 2369 | cli.Flag("path", "path to file").String(&path) 2370 | cli.Run(func(ctx context.Context) error { 2371 | called++ 2372 | return nil 2373 | }) 2374 | } 2375 | { 2376 | cli := cli.Command("fs:list", "list a directory") 2377 | cli.Flag("src", "source directory").String(&src) 2378 | cli.Flag("path", "path to directory").String(&path) 2379 | cli.Run(func(ctx context.Context) error { 2380 | called++ 2381 | return nil 2382 | }) 2383 | } 2384 | { 2385 | cli := cli.Command("fs:cp", "cp a directory") 2386 | cli.Arg("from", "from directory").String(&src) 2387 | cli.Arg("to", "to directory").String(&path) 2388 | cli.Flag("sync", "sync the directory").Bool(&sync) 2389 | cli.Run(func(ctx context.Context) error { 2390 | called++ 2391 | return nil 2392 | }) 2393 | } 2394 | ctx := context.Background() 2395 | err := cli.Parse(ctx, "fs:cat", "--src=http://url.com", "--path", "mypath", "-C", "cool") 2396 | is.NoErr(err) 2397 | is.Equal(1, called) 2398 | is.Equal(dir, "cool") 2399 | is.Equal(src, "http://url.com") 2400 | is.Equal(path, "mypath") 2401 | err = cli.Parse(ctx, "fs:list", "--src=http://url.com", "-C", "cool", "--path", "mypath") 2402 | is.NoErr(err) 2403 | is.Equal(2, called) 2404 | is.Equal(dir, "cool") 2405 | is.Equal(src, "http://url.com") 2406 | is.Equal(path, "mypath") 2407 | err = cli.Parse(ctx, "fs:cp", "some-from", "some-to", "-C", "cool", "--sync") 2408 | is.NoErr(err) 2409 | is.Equal(3, called) 2410 | is.Equal(src, "some-from") 2411 | is.Equal(path, "some-to") 2412 | is.Equal(dir, "cool") 2413 | is.Equal(sync, true) 2414 | } 2415 | 2416 | func TestFlagsConflictPanic(t *testing.T) { 2417 | is := is.New(t) 2418 | actual := new(bytes.Buffer) 2419 | var chdir string 2420 | var copy bool 2421 | called := 0 2422 | 2423 | cli := cli.New("bud", "bud cli").Writer(actual) 2424 | cli.Flag("chdir", "change the dir").Short('C').String(&chdir).Default(".") 2425 | 2426 | cmd := cli.Command("sub", "subcommand") 2427 | cmd.Flag("copy", "copy flag").Short('C').Bool(©) 2428 | cmd.Run(func(ctx context.Context) error { 2429 | called++ 2430 | return nil 2431 | }) 2432 | 2433 | ctx := context.Background() 2434 | err := cli.Parse(ctx, "-C", "dir", "sub", "--copy") 2435 | is.True(err != nil) 2436 | is.Equal(err.Error(), `cli: invalid input "bud sub" command contains a duplicate flag "-C"`) 2437 | } 2438 | 2439 | func TestFlagCopy(t *testing.T) { 2440 | is := is.New(t) 2441 | ctx := context.Background() 2442 | dir := "" 2443 | log := "" 2444 | limit := "" 2445 | key := "" 2446 | revision := "" 2447 | 2448 | actual := new(bytes.Buffer) 2449 | cli := cli.New("cli", "cli command").Writer(actual) 2450 | cli.Flag("dir", "dir").String(&dir).Default(".") 2451 | cli.Flag("log", "log level").Enum(&log, "debug", "info", "warn", "error").Default("info") 2452 | cli.Flag("limit-memory", "limit memory").String(&limit).Default("") 2453 | 2454 | { // logs 2455 | cmd := cli.Command("logs", "show logs for an app") 2456 | cmd.Flag("revision", "revision to log").Short('r').String(&revision).Default("latest") 2457 | } 2458 | 2459 | { // postinstall 2460 | cmd := cli.Command("postinstall", "post install script").Hidden() 2461 | cmd.Flag("public-key", "public key").String(&key) 2462 | } 2463 | 2464 | { // check 2465 | cmd := cli.Command("check", "run healthchecks") 2466 | cmd.Flag("revision", "revision to check").Short('r').String(&revision).Default("latest") 2467 | } 2468 | 2469 | // Ensure postinstall doesn't have revision 2470 | is.NoErr(cli.Parse(ctx, "postinstall", "-h")) 2471 | is.True(strings.Contains(actual.String(), "public-key")) 2472 | is.True(!strings.Contains(actual.String(), "revision")) 2473 | 2474 | // Reset the buffer 2475 | actual.Reset() 2476 | 2477 | // Ensure check doesn't have public-key 2478 | is.NoErr(cli.Parse(ctx, "check", "-h")) 2479 | is.True(strings.Contains(actual.String(), "revision")) 2480 | is.True(!strings.Contains(actual.String(), "public-key")) 2481 | } 2482 | 2483 | func TestFlagEnv(t *testing.T) { 2484 | is := is.New(t) 2485 | ctx := context.Background() 2486 | actual := new(bytes.Buffer) 2487 | cli := cli.New("cli", "cli command").Writer(actual) 2488 | verbose := false 2489 | log := "" 2490 | n := 0 2491 | dir := "" 2492 | mp := map[string]string{} 2493 | arr := []string{} 2494 | cli.Flag("verbose", "verbose").Env("VERBOSE").Bool(&verbose) 2495 | cli.Flag("log", "log level").Env("LOG").Enum(&log, "debug", "info", "warn", "error").Default("info") 2496 | cli.Flag("n", "n").Env("N").Int(&n) 2497 | cli.Flag("dir", "dir").Env("DIR").String(&dir) 2498 | cli.Flag("mp", "mp").Env("MP").StringMap(&mp) 2499 | cli.Flag("arr", "arr").Env("ARR").Strings(&arr) 2500 | 2501 | t.Setenv("VERBOSE", "true") 2502 | t.Setenv("LOG", "debug") 2503 | t.Setenv("N", "1") 2504 | t.Setenv("DIR", "dir") 2505 | t.Setenv("MP", "a:b c:'d e'") 2506 | t.Setenv("ARR", "a b 'c d'") 2507 | 2508 | is.NoErr(cli.Parse(ctx)) 2509 | 2510 | is.Equal(true, verbose) 2511 | is.Equal("debug", log) 2512 | is.Equal(1, n) 2513 | is.Equal("dir", dir) 2514 | 2515 | is.Equal(len(mp), 2) 2516 | is.Equal(mp["a"], "b") 2517 | is.Equal(mp["c"], "d e") 2518 | 2519 | is.Equal(3, len(arr)) 2520 | is.Equal(arr[0], "a") 2521 | is.Equal(arr[1], "b") 2522 | is.Equal(arr[2], "c d") 2523 | } 2524 | 2525 | func TestArgEnv(t *testing.T) { 2526 | is := is.New(t) 2527 | ctx := context.Background() 2528 | actual := new(bytes.Buffer) 2529 | cli := cli.New("cli", "cli command").Writer(actual) 2530 | verbose := false 2531 | log := "" 2532 | n := 0 2533 | dir := "" 2534 | mp := map[string]string{} 2535 | arr := []string{} 2536 | cli.Arg("verbose", "verbose").Env("VERBOSE").Bool(&verbose) 2537 | cli.Arg("log", "log level").Env("LOG").Enum(&log, "debug", "info", "warn", "error").Default("info") 2538 | cli.Arg("n", "n").Env("N").Int(&n) 2539 | cli.Arg("dir", "dir").Env("DIR").String(&dir) 2540 | cli.Arg("mp", "mp").Env("MP").StringMap(&mp) 2541 | cli.Args("arr", "arr").Env("ARR").Strings(&arr) 2542 | 2543 | t.Setenv("VERBOSE", "true") 2544 | t.Setenv("LOG", "debug") 2545 | t.Setenv("N", "1") 2546 | t.Setenv("DIR", "dir") 2547 | t.Setenv("MP", "a:b c:'d e'") 2548 | t.Setenv("ARR", "a b 'c d'") 2549 | 2550 | is.NoErr(cli.Parse(ctx)) 2551 | 2552 | is.Equal(true, verbose) 2553 | is.Equal("debug", log) 2554 | is.Equal(1, n) 2555 | is.Equal("dir", dir) 2556 | 2557 | is.Equal(len(mp), 2) 2558 | is.Equal(mp["a"], "b") 2559 | is.Equal(mp["c"], "d e") 2560 | 2561 | is.Equal(3, len(arr)) 2562 | is.Equal(arr[0], "a") 2563 | is.Equal(arr[1], "b") 2564 | is.Equal(arr[2], "c d") 2565 | } 2566 | 2567 | func TestFlagEnvFlagPriority(t *testing.T) { 2568 | is := is.New(t) 2569 | ctx := context.Background() 2570 | actual := new(bytes.Buffer) 2571 | cli := cli.New("cli", "cli command").Writer(actual) 2572 | verbose := false 2573 | log := "" 2574 | n := 0 2575 | dir := "" 2576 | mp := map[string]string{} 2577 | arr := []string{} 2578 | cli.Flag("verbose", "verbose").Env("VERBOSE").Bool(&verbose) 2579 | cli.Flag("log", "log level").Env("LOG").Enum(&log, "debug", "info", "warn", "error").Default("info") 2580 | cli.Flag("n", "n").Env("N").Int(&n) 2581 | cli.Flag("dir", "dir").Env("DIR").String(&dir) 2582 | cli.Flag("mp", "mp").Env("MP").StringMap(&mp) 2583 | cli.Flag("arr", "arr").Env("ARR").Strings(&arr) 2584 | 2585 | t.Setenv("VERBOSE", "true") 2586 | t.Setenv("LOG", "debug") 2587 | t.Setenv("N", "1") 2588 | t.Setenv("DIR", "dir") 2589 | t.Setenv("MP", "a:b,c:d") 2590 | t.Setenv("ARR", "a,b,c") 2591 | 2592 | is.NoErr(cli.Parse(ctx, 2593 | "--verbose=false", 2594 | "--log", "warn", 2595 | "--n", "2", 2596 | "--dir", "dir2", 2597 | "--mp", "e:f", 2598 | "--mp", "g:h", 2599 | "--arr", "d", 2600 | "--arr", "e", 2601 | "--arr", "f", 2602 | )) 2603 | 2604 | is.Equal(false, verbose) 2605 | is.Equal("warn", log) 2606 | is.Equal(2, n) 2607 | is.Equal("dir2", dir) 2608 | 2609 | is.Equal(len(mp), 2) 2610 | is.Equal(mp["e"], "f") 2611 | is.Equal(mp["g"], "h") 2612 | 2613 | is.Equal(3, len(arr)) 2614 | is.Equal(arr[0], "d") 2615 | is.Equal(arr[1], "e") 2616 | is.Equal(arr[2], "f") 2617 | } 2618 | 2619 | func TestFlagOptionalEnv(t *testing.T) { 2620 | is := is.New(t) 2621 | ctx := context.Background() 2622 | actual := new(bytes.Buffer) 2623 | cli := cli.New("cli", "cli command").Writer(actual) 2624 | var verbose *bool 2625 | var log *string 2626 | var n *int 2627 | var dir *string 2628 | var mp map[string]string 2629 | var arr []string 2630 | cli.Flag("verbose", "verbose").Env("VERBOSE").Optional().Bool(&verbose) 2631 | cli.Flag("log", "log level").Env("LOG").Optional().Enum(&log, "debug", "info", "warn", "error") 2632 | cli.Flag("n", "n").Env("N").Optional().Int(&n) 2633 | cli.Flag("dir", "dir").Env("DIR").Optional().String(&dir) 2634 | cli.Flag("mp", "mp").Env("MP").Optional().StringMap(&mp) 2635 | cli.Flag("arr", "arr").Env("ARR").Optional().Strings(&arr) 2636 | 2637 | is.NoErr(cli.Parse(ctx)) 2638 | 2639 | is.Equal(nil, verbose) 2640 | is.Equal(nil, log) 2641 | is.Equal(nil, n) 2642 | is.Equal(nil, dir) 2643 | 2644 | is.Equal(mp, nil) 2645 | is.Equal(arr, nil) 2646 | } 2647 | 2648 | func TestEnvHelp(t *testing.T) { 2649 | is := is.New(t) 2650 | ctx := context.Background() 2651 | actual := new(bytes.Buffer) 2652 | cli := cli.New("cli", "cli command").Writer(actual) 2653 | verbose := false 2654 | log := "" 2655 | n := 0 2656 | dir := "" 2657 | mp := map[string]string{} 2658 | arr := []string{} 2659 | cli.Flag("verbose", "verbose").Env("VERBOSE").Bool(&verbose) 2660 | cli.Flag("log", "log level").Env("LOG").Enum(&log, "debug", "info", "warn", "error").Default("info") 2661 | cli.Flag("n", "n").Env("N").Int(&n) 2662 | cli.Flag("dir", "dir").Env("DIR").String(&dir) 2663 | cli.Flag("mp", "mp").Env("MP").StringMap(&mp) 2664 | cli.Flag("arr", "arr").Env("ARR").Strings(&arr) 2665 | 2666 | is.NoErr(cli.Parse(ctx, "-h")) 2667 | 2668 | isEqual(t, actual.String(), ` 2669 | {bold}Usage:{reset} 2670 | {dim}${reset} cli {dim}[flags]{reset} 2671 | 2672 | {bold}Description:{reset} 2673 | cli command 2674 | 2675 | {bold}Flags:{reset} 2676 | --arr {dim}arr (or $ARR){reset} 2677 | --dir {dim}dir (or $DIR){reset} 2678 | --log {dim}log level (or $LOG, default:"info"){reset} 2679 | --mp {dim}mp (or $MP){reset} 2680 | --n {dim}n (or $N){reset} 2681 | --verbose {dim}verbose (or $VERBOSE){reset} 2682 | 2683 | `) 2684 | } 2685 | 2686 | func TestDashDash(t *testing.T) { 2687 | is := is.New(t) 2688 | actual := new(bytes.Buffer) 2689 | called := 0 2690 | cli := cli.New("cli", "cli command").Writer(actual) 2691 | var w, x, y, z *string 2692 | cli.Arg("w", "w arg").Optional().String(&w) 2693 | cli.Arg("x", "x arg").Optional().String(&x) 2694 | cli.Flag("y", "y flag").Optional().String(&y) 2695 | cli.Flag("z", "z flag").Optional().String(&z) 2696 | cli.Run(func(ctx context.Context) error { 2697 | called++ 2698 | return nil 2699 | }) 2700 | ctx := context.Background() 2701 | err := cli.Parse(ctx, "w", "-y", "y", "--", "-z", "z", "foo") 2702 | is.NoErr(err) 2703 | is.Equal(1, called) 2704 | is.Equal(*w, "w") 2705 | is.Equal(*x, "-z z foo") 2706 | is.Equal(*y, "y") 2707 | is.Equal(z, nil) 2708 | } 2709 | 2710 | func TestDashDashNoArgs(t *testing.T) { 2711 | is := is.New(t) 2712 | actual := new(bytes.Buffer) 2713 | called := 0 2714 | cli := cli.New("cli", "cli command").Writer(actual) 2715 | var w, x, y, z *string 2716 | cli.Arg("w", "w arg").Optional().String(&w) 2717 | cli.Arg("x", "x arg").Optional().String(&x) 2718 | cli.Flag("y", "y flag").Optional().String(&y) 2719 | cli.Flag("z", "z flag").Optional().String(&z) 2720 | cli.Run(func(ctx context.Context) error { 2721 | called++ 2722 | return nil 2723 | }) 2724 | ctx := context.Background() 2725 | err := cli.Parse(ctx, "--", "w", "-y", "y", "-z", "z", "foo") 2726 | is.NoErr(err) 2727 | is.Equal(1, called) 2728 | is.Equal(*w, "w -y y -z z foo") 2729 | is.Equal(x, nil) 2730 | is.Equal(y, nil) 2731 | is.Equal(z, nil) 2732 | } 2733 | 2734 | func TestCommandDashDash(t *testing.T) { 2735 | is := is.New(t) 2736 | actual := new(bytes.Buffer) 2737 | called := 0 2738 | cli := cli.New("dev", "dev command").Writer(actual) 2739 | cmd := cli.Command("watch", "watch command") 2740 | var clear bool 2741 | cmd.Flag("clear", "clear").Bool(&clear) 2742 | var command string 2743 | cmd.Arg("command", "command to watch").String(&command) 2744 | cmd.Run(func(ctx context.Context) error { 2745 | called++ 2746 | return nil 2747 | }) 2748 | ctx := context.Background() 2749 | err := cli.Parse(ctx, "watch", "--clear", "--", "go", "test", "./mktempl", "-v", "-failfast", "-run", "TestParse/multiple-packages") 2750 | is.NoErr(err) 2751 | is.Equal(1, called) 2752 | is.Equal(clear, true) 2753 | is.Equal(command, "go test ./mktempl -v -failfast -run TestParse/multiple-packages") 2754 | } 2755 | 2756 | func TestMissingSetter(t *testing.T) { 2757 | is := is.New(t) 2758 | actual := new(bytes.Buffer) 2759 | called := 0 2760 | cli := cli.New("cli", "desc").Writer(actual) 2761 | cli.Run(func(ctx context.Context) error { 2762 | called++ 2763 | return nil 2764 | }) 2765 | cli.Flag("flag", "cli flag") 2766 | ctx := context.Background() 2767 | err := cli.Parse(ctx) 2768 | is.True(err != nil) 2769 | is.Equal(err.Error(), `cli: invalid input "cli" command flag "flag" is missing a value setter`) 2770 | } 2771 | 2772 | func TestMissingRunTriggersHelp(t *testing.T) { 2773 | is := is.New(t) 2774 | actual := new(bytes.Buffer) 2775 | cli := cli.New("cli", "desc").Writer(actual) 2776 | ctx := context.Background() 2777 | err := cli.Parse(ctx) 2778 | is.NoErr(err) 2779 | isEqual(t, actual.String(), ` 2780 | {bold}Usage:{reset} 2781 | {dim}${reset} cli 2782 | 2783 | {bold}Description:{reset} 2784 | desc 2785 | 2786 | `) 2787 | } 2788 | 2789 | func TestMiddlewareOrder(t *testing.T) { 2790 | is := is.New(t) 2791 | actual := new(bytes.Buffer) 2792 | cli := cli.New("cli", "middleware order").Writer(actual) 2793 | trace := []string{} 2794 | cli.Use(func(next func(ctx context.Context) error) func(ctx context.Context) error { 2795 | return func(ctx context.Context) error { 2796 | trace = append(trace, "root-mw") 2797 | return next(ctx) 2798 | } 2799 | }) 2800 | { 2801 | sub := cli.Command("sub", "sub command") 2802 | sub.Use(func(next func(ctx context.Context) error) func(ctx context.Context) error { 2803 | return func(ctx context.Context) error { 2804 | trace = append(trace, "sub-mw") 2805 | return next(ctx) 2806 | } 2807 | }) 2808 | sub.Use(func(next func(ctx context.Context) error) func(ctx context.Context) error { 2809 | return func(ctx context.Context) error { 2810 | trace = append(trace, "sub-mw-2") 2811 | return next(ctx) 2812 | } 2813 | }) 2814 | sub.Run(func(ctx context.Context) error { 2815 | trace = append(trace, "sub-run") 2816 | return nil 2817 | }) 2818 | } 2819 | ctx := context.Background() 2820 | err := cli.Parse(ctx, "sub") 2821 | is.NoErr(err) 2822 | is.Equal(trace, []string{"sub-mw", "sub-mw-2", "sub-run"}) 2823 | } 2824 | 2825 | func TestMiddlewareError(t *testing.T) { 2826 | is := is.New(t) 2827 | actual := new(bytes.Buffer) 2828 | cli := cli.New("cli", "middleware error").Writer(actual) 2829 | trace := []string{} 2830 | cli.Use(func(next func(ctx context.Context) error) func(ctx context.Context) error { 2831 | return func(ctx context.Context) error { 2832 | trace = append(trace, "mw") 2833 | return errors.New("stop") 2834 | } 2835 | }) 2836 | cli.Run(func(ctx context.Context) error { 2837 | trace = append(trace, "run") 2838 | return nil 2839 | }) 2840 | ctx := context.Background() 2841 | err := cli.Parse(ctx) 2842 | is.True(err != nil) 2843 | is.Equal(err.Error(), "stop") 2844 | is.Equal(trace, []string{"mw"}) 2845 | } 2846 | 2847 | func TestMiddlewareContext(t *testing.T) { 2848 | is := is.New(t) 2849 | actual := new(bytes.Buffer) 2850 | cli := cli.New("cli", "middleware context").Writer(actual) 2851 | type key string 2852 | const testKey key = "test" 2853 | cli.Use(func(next func(ctx context.Context) error) func(ctx context.Context) error { 2854 | return func(ctx context.Context) error { 2855 | ctx = context.WithValue(ctx, testKey, "value") 2856 | return next(ctx) 2857 | } 2858 | }) 2859 | cli.Run(func(ctx context.Context) error { 2860 | val := ctx.Value(testKey) 2861 | if valStr, ok := val.(string); ok { 2862 | actual.WriteString(valStr) 2863 | } 2864 | return nil 2865 | }) 2866 | ctx := context.Background() 2867 | err := cli.Parse(ctx) 2868 | is.NoErr(err) 2869 | is.Equal(actual.String(), "value") 2870 | } 2871 | 2872 | func TestArgDash(t *testing.T) { 2873 | is := is.New(t) 2874 | actual := new(bytes.Buffer) 2875 | called := 0 2876 | cli := cli.New("cli", "cli command").Writer(actual) 2877 | var s string 2878 | cli.Arg("s", "string arg").String(&s) 2879 | cli.Run(func(ctx context.Context) error { 2880 | called++ 2881 | return nil 2882 | }) 2883 | ctx := context.Background() 2884 | err := cli.Parse(ctx, "-") 2885 | is.NoErr(err) 2886 | is.Equal(1, called) 2887 | is.Equal(s, "-") 2888 | } 2889 | --------------------------------------------------------------------------------