├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── arg.go ├── arg_test.go ├── assert_test.go ├── check_crosscompile.sh ├── closest.go ├── command.go ├── command_test.go ├── completion.go ├── completion_test.go ├── convert.go ├── convert_test.go ├── error.go ├── example_test.go ├── examples ├── add.go ├── bash-completion ├── main.go └── rm.go ├── flags.go ├── go.mod ├── go.sum ├── group.go ├── group_test.go ├── help.go ├── help_test.go ├── ini.go ├── ini_test.go ├── long_test.go ├── man.go ├── marshal_test.go ├── multitag.go ├── option.go ├── options_test.go ├── optstyle_other.go ├── optstyle_windows.go ├── parser.go ├── parser_test.go ├── pointer_test.go ├── short_test.go ├── termsize.go ├── termsize_defaults.go ├── termsize_nosysioctl.go ├── termsize_windows.go └── unknown_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: ["main"] 10 | pull_request: 11 | branches: ["main"] 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: setup 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version: "1.20" 26 | 27 | - name: build 28 | run: go build -v ./... 29 | 30 | - name: test 31 | run: go test -v ./... 32 | 33 | - name: gofmt 34 | if: matrix.os == 'ubuntu-latest' 35 | run: exit $(gofmt -l . | wc -l) 36 | 37 | - name: vet 38 | if: matrix.os == 'ubuntu-latest' 39 | run: go vet -all=true -v=true . 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jesse van den Kieboom. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following disclaimer 10 | in the documentation and/or other materials provided with the 11 | distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived from 14 | this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-flags: a go library for parsing command line arguments 2 | ========================================================= 3 | 4 | [![GoDoc](https://godoc.org/github.com/jessevdk/go-flags?status.png)](https://godoc.org/github.com/jessevdk/go-flags) 5 | 6 | This library provides similar functionality to the builtin flag library of 7 | go, but provides much more functionality and nicer formatting. From the 8 | documentation: 9 | 10 | Package flags provides an extensive command line option parser. 11 | The flags package is similar in functionality to the go builtin flag package 12 | but provides more options and uses reflection to provide a convenient and 13 | succinct way of specifying command line options. 14 | 15 | Supported features: 16 | * Options with short names (-v) 17 | * Options with long names (--verbose) 18 | * Options with and without arguments (bool v.s. other type) 19 | * Options with optional arguments and default values 20 | * Multiple option groups each containing a set of options 21 | * Generate and print well-formatted help message 22 | * Passing remaining command line arguments after -- (optional) 23 | * Ignoring unknown command line options (optional) 24 | * Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification 25 | * Supports multiple short options -aux 26 | * Supports all primitive go types (string, int{8..64}, uint{8..64}, float) 27 | * Supports same option multiple times (can store in slice or last option counts) 28 | * Supports maps 29 | * Supports function callbacks 30 | * Supports namespaces for (nested) option groups 31 | 32 | The flags package uses structs, reflection and struct field tags 33 | to allow users to specify command line options. This results in very simple 34 | and concise specification of your application options. For example: 35 | 36 | ```go 37 | type Options struct { 38 | Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` 39 | } 40 | ``` 41 | 42 | This specifies one option with a short name -v and a long name --verbose. 43 | When either -v or --verbose is found on the command line, a 'true' value 44 | will be appended to the Verbose field. e.g. when specifying -vvv, the 45 | resulting value of Verbose will be {[true, true, true]}. 46 | 47 | Example: 48 | -------- 49 | ```go 50 | var opts struct { 51 | // Slice of bool will append 'true' each time the option 52 | // is encountered (can be set multiple times, like -vvv) 53 | Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` 54 | 55 | // Example of automatic marshalling to desired type (uint) 56 | Offset uint `long:"offset" description:"Offset"` 57 | 58 | // Example of a callback, called each time the option is found. 59 | Call func(string) `short:"c" description:"Call phone number"` 60 | 61 | // Example of a required flag 62 | Name string `short:"n" long:"name" description:"A name" required:"true"` 63 | 64 | // Example of a flag restricted to a pre-defined set of strings 65 | Animal string `long:"animal" choice:"cat" choice:"dog"` 66 | 67 | // Example of a value name 68 | File string `short:"f" long:"file" description:"A file" value-name:"FILE"` 69 | 70 | // Example of a pointer 71 | Ptr *int `short:"p" description:"A pointer to an integer"` 72 | 73 | // Example of a slice of strings 74 | StringSlice []string `short:"s" description:"A slice of strings"` 75 | 76 | // Example of a slice of pointers 77 | PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` 78 | 79 | // Example of a map 80 | IntMap map[string]int `long:"intmap" description:"A map from string to int"` 81 | 82 | // Example of env variable 83 | Thresholds []int `long:"thresholds" default:"1" default:"2" env:"THRESHOLD_VALUES" env-delim:","` 84 | } 85 | 86 | // Callback which will invoke callto: to call a number. 87 | // Note that this works just on OS X (and probably only with 88 | // Skype) but it shows the idea. 89 | opts.Call = func(num string) { 90 | cmd := exec.Command("open", "callto:"+num) 91 | cmd.Start() 92 | cmd.Process.Release() 93 | } 94 | 95 | // Make some fake arguments to parse. 96 | args := []string{ 97 | "-vv", 98 | "--offset=5", 99 | "-n", "Me", 100 | "--animal", "dog", // anything other than "cat" or "dog" will raise an error 101 | "-p", "3", 102 | "-s", "hello", 103 | "-s", "world", 104 | "--ptrslice", "hello", 105 | "--ptrslice", "world", 106 | "--intmap", "a:1", 107 | "--intmap", "b:5", 108 | "arg1", 109 | "arg2", 110 | "arg3", 111 | } 112 | 113 | // Parse flags from `args'. Note that here we use flags.ParseArgs for 114 | // the sake of making a working example. Normally, you would simply use 115 | // flags.Parse(&opts) which uses os.Args 116 | args, err := flags.ParseArgs(&opts, args) 117 | 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | fmt.Printf("Verbosity: %v\n", opts.Verbose) 123 | fmt.Printf("Offset: %d\n", opts.Offset) 124 | fmt.Printf("Name: %s\n", opts.Name) 125 | fmt.Printf("Animal: %s\n", opts.Animal) 126 | fmt.Printf("Ptr: %d\n", *opts.Ptr) 127 | fmt.Printf("StringSlice: %v\n", opts.StringSlice) 128 | fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1]) 129 | fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"]) 130 | fmt.Printf("Remaining args: %s\n", strings.Join(args, " ")) 131 | 132 | // Output: Verbosity: [true true] 133 | // Offset: 5 134 | // Name: Me 135 | // Ptr: 3 136 | // StringSlice: [hello world] 137 | // PtrSlice: [hello world] 138 | // IntMap: [a:1 b:5] 139 | // Remaining args: arg1 arg2 arg3 140 | ``` 141 | 142 | More information can be found in the godocs: 143 | -------------------------------------------------------------------------------- /arg.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // Arg represents a positional argument on the command line. 8 | type Arg struct { 9 | // The name of the positional argument (used in the help) 10 | Name string 11 | 12 | // A description of the positional argument (used in the help) 13 | Description string 14 | 15 | // The minimal number of required positional arguments 16 | Required int 17 | 18 | // The maximum number of required positional arguments 19 | RequiredMaximum int 20 | 21 | value reflect.Value 22 | tag multiTag 23 | } 24 | 25 | func (a *Arg) isRemaining() bool { 26 | return a.value.Type().Kind() == reflect.Slice 27 | } 28 | -------------------------------------------------------------------------------- /arg_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPositional(t *testing.T) { 8 | var opts = struct { 9 | Value bool `short:"v"` 10 | 11 | Positional struct { 12 | Command int 13 | Filename string 14 | Rest []string 15 | } `positional-args:"yes" required:"yes"` 16 | }{} 17 | 18 | p := NewParser(&opts, Default) 19 | ret, err := p.ParseArgs([]string{"10", "arg_test.go", "a", "b"}) 20 | 21 | if err != nil { 22 | t.Fatalf("Unexpected error: %v", err) 23 | return 24 | } 25 | 26 | if opts.Positional.Command != 10 { 27 | t.Fatalf("Expected opts.Positional.Command to be 10, but got %v", opts.Positional.Command) 28 | } 29 | 30 | if opts.Positional.Filename != "arg_test.go" { 31 | t.Fatalf("Expected opts.Positional.Filename to be \"arg_test.go\", but got %v", opts.Positional.Filename) 32 | } 33 | 34 | assertStringArray(t, opts.Positional.Rest, []string{"a", "b"}) 35 | assertStringArray(t, ret, []string{}) 36 | } 37 | 38 | func TestPositionalRequired(t *testing.T) { 39 | var opts = struct { 40 | Value bool `short:"v"` 41 | 42 | Positional struct { 43 | Command int 44 | Filename string 45 | Rest []string 46 | } `positional-args:"yes" required:"yes"` 47 | }{} 48 | 49 | p := NewParser(&opts, None) 50 | _, err := p.ParseArgs([]string{"10"}) 51 | 52 | assertError(t, err, ErrRequired, "the required argument `Filename` was not provided") 53 | } 54 | 55 | func TestPositionalRequiredRest1Fail(t *testing.T) { 56 | var opts = struct { 57 | Value bool `short:"v"` 58 | 59 | Positional struct { 60 | Rest []string `required:"yes"` 61 | } `positional-args:"yes"` 62 | }{} 63 | 64 | p := NewParser(&opts, None) 65 | _, err := p.ParseArgs([]string{}) 66 | 67 | assertError(t, err, ErrRequired, "the required argument `Rest (at least 1 argument)` was not provided") 68 | } 69 | 70 | func TestPositionalRequiredRest1Pass(t *testing.T) { 71 | var opts = struct { 72 | Value bool `short:"v"` 73 | 74 | Positional struct { 75 | Rest []string `required:"yes"` 76 | } `positional-args:"yes"` 77 | }{} 78 | 79 | p := NewParser(&opts, None) 80 | _, err := p.ParseArgs([]string{"rest1"}) 81 | 82 | if err != nil { 83 | t.Fatalf("Unexpected error: %v", err) 84 | return 85 | } 86 | 87 | if len(opts.Positional.Rest) != 1 { 88 | t.Fatalf("Expected 1 positional rest argument") 89 | } 90 | 91 | assertString(t, opts.Positional.Rest[0], "rest1") 92 | } 93 | 94 | func TestPositionalRequiredRest2Fail(t *testing.T) { 95 | var opts = struct { 96 | Value bool `short:"v"` 97 | 98 | Positional struct { 99 | Rest []string `required:"2"` 100 | } `positional-args:"yes"` 101 | }{} 102 | 103 | p := NewParser(&opts, None) 104 | _, err := p.ParseArgs([]string{"rest1"}) 105 | 106 | assertError(t, err, ErrRequired, "the required argument `Rest (at least 2 arguments, but got only 1)` was not provided") 107 | } 108 | 109 | func TestPositionalRequiredRest2Pass(t *testing.T) { 110 | var opts = struct { 111 | Value bool `short:"v"` 112 | 113 | Positional struct { 114 | Rest []string `required:"2"` 115 | } `positional-args:"yes"` 116 | }{} 117 | 118 | p := NewParser(&opts, None) 119 | _, err := p.ParseArgs([]string{"rest1", "rest2", "rest3"}) 120 | 121 | if err != nil { 122 | t.Fatalf("Unexpected error: %v", err) 123 | return 124 | } 125 | 126 | if len(opts.Positional.Rest) != 3 { 127 | t.Fatalf("Expected 3 positional rest argument") 128 | } 129 | 130 | assertString(t, opts.Positional.Rest[0], "rest1") 131 | assertString(t, opts.Positional.Rest[1], "rest2") 132 | assertString(t, opts.Positional.Rest[2], "rest3") 133 | } 134 | 135 | func TestPositionalRequiredRestRangeFail(t *testing.T) { 136 | var opts = struct { 137 | Value bool `short:"v"` 138 | 139 | Positional struct { 140 | Rest []string `required:"1-2"` 141 | } `positional-args:"yes"` 142 | }{} 143 | 144 | p := NewParser(&opts, None) 145 | _, err := p.ParseArgs([]string{"rest1", "rest2", "rest3"}) 146 | 147 | assertError(t, err, ErrRequired, "the required argument `Rest (at most 2 arguments, but got 3)` was not provided") 148 | } 149 | 150 | func TestPositionalRequiredRestRangeEmptyFail(t *testing.T) { 151 | var opts = struct { 152 | Value bool `short:"v"` 153 | 154 | Positional struct { 155 | Rest []string `required:"0-0"` 156 | } `positional-args:"yes"` 157 | }{} 158 | 159 | p := NewParser(&opts, None) 160 | _, err := p.ParseArgs([]string{"some", "thing"}) 161 | 162 | assertError(t, err, ErrRequired, "the required argument `Rest (zero arguments)` was not provided") 163 | } 164 | -------------------------------------------------------------------------------- /assert_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/sergi/go-diff/diffmatchpatch" 10 | ) 11 | 12 | func assertCallerInfo() (string, int) { 13 | ptr := make([]uintptr, 15) 14 | n := runtime.Callers(1, ptr) 15 | 16 | if n == 0 { 17 | return "", 0 18 | } 19 | 20 | mef := runtime.FuncForPC(ptr[0]) 21 | mefile, meline := mef.FileLine(ptr[0]) 22 | 23 | for i := 2; i < n; i++ { 24 | f := runtime.FuncForPC(ptr[i]) 25 | file, line := f.FileLine(ptr[i]) 26 | 27 | if file != mefile { 28 | return file, line 29 | } 30 | } 31 | 32 | return mefile, meline 33 | } 34 | 35 | func assertErrorf(t *testing.T, format string, args ...interface{}) { 36 | msg := fmt.Sprintf(format, args...) 37 | 38 | file, line := assertCallerInfo() 39 | 40 | t.Errorf("%s:%d: %s", path.Base(file), line, msg) 41 | } 42 | 43 | func assertFatalf(t *testing.T, format string, args ...interface{}) { 44 | msg := fmt.Sprintf(format, args...) 45 | 46 | file, line := assertCallerInfo() 47 | 48 | t.Fatalf("%s:%d: %s", path.Base(file), line, msg) 49 | } 50 | 51 | func assertString(t *testing.T, a string, b string) { 52 | if a != b { 53 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 54 | } 55 | } 56 | 57 | func assertStringArray(t *testing.T, a []string, b []string) { 58 | if len(a) != len(b) { 59 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 60 | return 61 | } 62 | 63 | for i, v := range a { 64 | if b[i] != v { 65 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 66 | return 67 | } 68 | } 69 | } 70 | 71 | func assertBoolArray(t *testing.T, a []bool, b []bool) { 72 | if len(a) != len(b) { 73 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 74 | return 75 | } 76 | 77 | for i, v := range a { 78 | if b[i] != v { 79 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 80 | return 81 | } 82 | } 83 | } 84 | 85 | func assertParserSuccess(t *testing.T, data interface{}, args ...string) (*Parser, []string) { 86 | parser := NewParser(data, Default&^PrintErrors) 87 | ret, err := parser.ParseArgs(args) 88 | 89 | if err != nil { 90 | t.Fatalf("Unexpected parse error: %s", err) 91 | return nil, nil 92 | } 93 | 94 | return parser, ret 95 | } 96 | 97 | func assertParseSuccess(t *testing.T, data interface{}, args ...string) []string { 98 | _, ret := assertParserSuccess(t, data, args...) 99 | return ret 100 | } 101 | 102 | func assertError(t *testing.T, err error, typ ErrorType, msg string) { 103 | if err == nil { 104 | assertFatalf(t, "Expected error: \"%s\", but no error occurred", msg) 105 | return 106 | } 107 | 108 | if e, ok := err.(*Error); !ok { 109 | assertFatalf(t, "Expected Error type, but got %#v", err) 110 | } else { 111 | if e.Type != typ { 112 | assertErrorf(t, "Expected error type {%s}, but got {%s}", typ, e.Type) 113 | } 114 | 115 | if e.Message != msg { 116 | assertErrorf(t, "Expected error message %#v, but got %#v", msg, e.Message) 117 | } 118 | } 119 | } 120 | 121 | func assertParseFail(t *testing.T, typ ErrorType, msg string, data interface{}, args ...string) []string { 122 | parser := NewParser(data, Default&^PrintErrors) 123 | ret, err := parser.ParseArgs(args) 124 | 125 | assertError(t, err, typ, msg) 126 | return ret 127 | } 128 | 129 | func assertDiff(t *testing.T, actual, expected, msg string) { 130 | if actual == expected { 131 | return 132 | } 133 | 134 | dmp := diffmatchpatch.New() 135 | diffs := dmp.DiffMain(actual, expected, false) 136 | 137 | if len(diffs) == 0 { 138 | return 139 | } 140 | 141 | pretty := dmp.DiffPrettyText(diffs) 142 | assertErrorf(t, "Unexpected %s:\n\n%s", msg, pretty) 143 | } 144 | -------------------------------------------------------------------------------- /check_crosscompile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo '# linux arm7' 6 | GOARM=7 GOARCH=arm GOOS=linux go build 7 | echo '# linux arm5' 8 | GOARM=5 GOARCH=arm GOOS=linux go build 9 | echo '# windows 386' 10 | GOARCH=386 GOOS=windows go build 11 | echo '# windows amd64' 12 | GOARCH=amd64 GOOS=windows go build 13 | echo '# darwin' 14 | GOARCH=amd64 GOOS=darwin go build 15 | echo '# freebsd' 16 | GOARCH=amd64 GOOS=freebsd go build 17 | echo '# aix ppc64' 18 | GOARCH=ppc64 GOOS=aix go build 19 | echo '# solaris amd64' 20 | GOARCH=amd64 GOOS=solaris go build 21 | -------------------------------------------------------------------------------- /closest.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | func levenshtein(s string, t string) int { 4 | if len(s) == 0 { 5 | return len(t) 6 | } 7 | 8 | if len(t) == 0 { 9 | return len(s) 10 | } 11 | 12 | dists := make([][]int, len(s)+1) 13 | for i := range dists { 14 | dists[i] = make([]int, len(t)+1) 15 | dists[i][0] = i 16 | } 17 | 18 | for j := range t { 19 | dists[0][j] = j 20 | } 21 | 22 | for i, sc := range s { 23 | for j, tc := range t { 24 | if sc == tc { 25 | dists[i+1][j+1] = dists[i][j] 26 | } else { 27 | dists[i+1][j+1] = dists[i][j] + 1 28 | if dists[i+1][j] < dists[i+1][j+1] { 29 | dists[i+1][j+1] = dists[i+1][j] + 1 30 | } 31 | if dists[i][j+1] < dists[i+1][j+1] { 32 | dists[i+1][j+1] = dists[i][j+1] + 1 33 | } 34 | } 35 | } 36 | } 37 | 38 | return dists[len(s)][len(t)] 39 | } 40 | 41 | func closestChoice(cmd string, choices []string) (string, int) { 42 | if len(choices) == 0 { 43 | return "", 0 44 | } 45 | 46 | mincmd := -1 47 | mindist := -1 48 | 49 | for i, c := range choices { 50 | l := levenshtein(cmd, c) 51 | 52 | if mincmd < 0 || l < mindist { 53 | mindist = l 54 | mincmd = i 55 | } 56 | } 57 | 58 | return choices[mincmd], mindist 59 | } 60 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Command represents an application command. Commands can be added to the 11 | // parser (which itself is a command) and are selected/executed when its name 12 | // is specified on the command line. The Command type embeds a Group and 13 | // therefore also carries a set of command specific options. 14 | type Command struct { 15 | // Embedded, see Group for more information 16 | *Group 17 | 18 | // The name by which the command can be invoked 19 | Name string 20 | 21 | // The active sub command (set by parsing) or nil 22 | Active *Command 23 | 24 | // Whether subcommands are optional 25 | SubcommandsOptional bool 26 | 27 | // Aliases for the command 28 | Aliases []string 29 | 30 | // Whether positional arguments are required 31 | ArgsRequired bool 32 | 33 | // Whether to pass all arguments after the first non option as remaining 34 | // command line arguments. This is equivalent to strict POSIX processing. 35 | // This is command-local version of PassAfterNonOption Parser flag. It 36 | // cannot be turned off when PassAfterNonOption Parser flag is set. 37 | PassAfterNonOption bool 38 | 39 | commands []*Command 40 | hasBuiltinHelpGroup bool 41 | args []*Arg 42 | } 43 | 44 | // Commander is an interface which can be implemented by any command added in 45 | // the options. When implemented, the Execute method will be called for the last 46 | // specified (sub)command providing the remaining command line arguments. 47 | type Commander interface { 48 | // Execute will be called for the last active (sub)command. The 49 | // args argument contains the remaining command line arguments. The 50 | // error that Execute returns will be eventually passed out of the 51 | // Parse method of the Parser. 52 | Execute(args []string) error 53 | } 54 | 55 | // Usage is an interface which can be implemented to show a custom usage string 56 | // in the help message shown for a command. 57 | type Usage interface { 58 | // Usage is called for commands to allow customized printing of command 59 | // usage in the generated help message. 60 | Usage() string 61 | } 62 | 63 | type lookup struct { 64 | shortNames map[string]*Option 65 | longNames map[string]*Option 66 | 67 | commands map[string]*Command 68 | } 69 | 70 | // AddCommand adds a new command to the parser with the given name and data. The 71 | // data needs to be a pointer to a struct from which the fields indicate which 72 | // options are in the command. The provided data can implement the Command and 73 | // Usage interfaces. 74 | func (c *Command) AddCommand(command string, shortDescription string, longDescription string, data interface{}) (*Command, error) { 75 | cmd := newCommand(command, shortDescription, longDescription, data) 76 | 77 | cmd.parent = c 78 | 79 | if err := cmd.scan(); err != nil { 80 | return nil, err 81 | } 82 | 83 | c.commands = append(c.commands, cmd) 84 | return cmd, nil 85 | } 86 | 87 | // AddGroup adds a new group to the command with the given name and data. The 88 | // data needs to be a pointer to a struct from which the fields indicate which 89 | // options are in the group. 90 | func (c *Command) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) { 91 | group := newGroup(shortDescription, longDescription, data) 92 | 93 | group.parent = c 94 | 95 | if err := group.scanType(c.scanSubcommandHandler(group)); err != nil { 96 | return nil, err 97 | } 98 | 99 | c.groups = append(c.groups, group) 100 | return group, nil 101 | } 102 | 103 | // Commands returns a list of subcommands of this command. 104 | func (c *Command) Commands() []*Command { 105 | return c.commands 106 | } 107 | 108 | // Find locates the subcommand with the given name and returns it. If no such 109 | // command can be found Find will return nil. 110 | func (c *Command) Find(name string) *Command { 111 | for _, cc := range c.commands { 112 | if cc.match(name) { 113 | return cc 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | 120 | // FindOptionByLongName finds an option that is part of the command, or any of 121 | // its parent commands, by matching its long name (including the option 122 | // namespace). 123 | func (c *Command) FindOptionByLongName(longName string) (option *Option) { 124 | for option == nil && c != nil { 125 | option = c.Group.FindOptionByLongName(longName) 126 | 127 | c, _ = c.parent.(*Command) 128 | } 129 | 130 | return option 131 | } 132 | 133 | // FindOptionByShortName finds an option that is part of the command, or any of 134 | // its parent commands, by matching its long name (including the option 135 | // namespace). 136 | func (c *Command) FindOptionByShortName(shortName rune) (option *Option) { 137 | for option == nil && c != nil { 138 | option = c.Group.FindOptionByShortName(shortName) 139 | 140 | c, _ = c.parent.(*Command) 141 | } 142 | 143 | return option 144 | } 145 | 146 | // Args returns a list of positional arguments associated with this command. 147 | func (c *Command) Args() []*Arg { 148 | ret := make([]*Arg, len(c.args)) 149 | copy(ret, c.args) 150 | 151 | return ret 152 | } 153 | 154 | func newCommand(name string, shortDescription string, longDescription string, data interface{}) *Command { 155 | return &Command{ 156 | Group: newGroup(shortDescription, longDescription, data), 157 | Name: name, 158 | } 159 | } 160 | 161 | func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { 162 | f := func(realval reflect.Value, sfield *reflect.StructField) (bool, error) { 163 | mtag := newMultiTag(string(sfield.Tag)) 164 | 165 | if err := mtag.Parse(); err != nil { 166 | return true, err 167 | } 168 | 169 | positional := mtag.Get("positional-args") 170 | 171 | if len(positional) != 0 { 172 | stype := realval.Type() 173 | 174 | for i := 0; i < stype.NumField(); i++ { 175 | field := stype.Field(i) 176 | 177 | m := newMultiTag((string(field.Tag))) 178 | 179 | if err := m.Parse(); err != nil { 180 | return true, err 181 | } 182 | 183 | name := m.Get("positional-arg-name") 184 | 185 | if len(name) == 0 { 186 | name = field.Name 187 | } 188 | 189 | required := -1 190 | requiredMaximum := -1 191 | 192 | sreq := m.Get("required") 193 | 194 | if sreq != "" { 195 | required = 1 196 | 197 | rng := strings.SplitN(sreq, "-", 2) 198 | 199 | if len(rng) > 1 { 200 | if preq, err := strconv.ParseInt(rng[0], 10, 32); err == nil { 201 | required = int(preq) 202 | } 203 | 204 | if preq, err := strconv.ParseInt(rng[1], 10, 32); err == nil { 205 | requiredMaximum = int(preq) 206 | } 207 | } else { 208 | if preq, err := strconv.ParseInt(sreq, 10, 32); err == nil { 209 | required = int(preq) 210 | } 211 | } 212 | } 213 | 214 | arg := &Arg{ 215 | Name: name, 216 | Description: m.Get("description"), 217 | Required: required, 218 | RequiredMaximum: requiredMaximum, 219 | 220 | value: realval.Field(i), 221 | tag: m, 222 | } 223 | 224 | c.args = append(c.args, arg) 225 | 226 | if len(mtag.Get("required")) != 0 { 227 | c.ArgsRequired = true 228 | } 229 | } 230 | 231 | return true, nil 232 | } 233 | 234 | subcommand := mtag.Get("command") 235 | 236 | if len(subcommand) != 0 { 237 | var ptrval reflect.Value 238 | 239 | if realval.Kind() == reflect.Ptr { 240 | ptrval = realval 241 | 242 | if ptrval.IsNil() { 243 | ptrval.Set(reflect.New(ptrval.Type().Elem())) 244 | } 245 | } else { 246 | ptrval = realval.Addr() 247 | } 248 | 249 | shortDescription := mtag.Get("description") 250 | longDescription := mtag.Get("long-description") 251 | subcommandsOptional := mtag.Get("subcommands-optional") 252 | aliases := mtag.GetMany("alias") 253 | passAfterNonOption := mtag.Get("pass-after-non-option") 254 | 255 | subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) 256 | 257 | if err != nil { 258 | return true, err 259 | } 260 | 261 | subc.Hidden = mtag.Get("hidden") != "" 262 | 263 | if len(subcommandsOptional) > 0 { 264 | subc.SubcommandsOptional = true 265 | } 266 | 267 | if len(aliases) > 0 { 268 | subc.Aliases = aliases 269 | } 270 | 271 | if len(passAfterNonOption) > 0 { 272 | subc.PassAfterNonOption = true 273 | } 274 | 275 | return true, nil 276 | } 277 | 278 | return parentg.scanSubGroupHandler(realval, sfield) 279 | } 280 | 281 | return f 282 | } 283 | 284 | func (c *Command) scan() error { 285 | return c.scanType(c.scanSubcommandHandler(c.Group)) 286 | } 287 | 288 | func (c *Command) eachOption(f func(*Command, *Group, *Option)) { 289 | c.eachCommand(func(c *Command) { 290 | c.eachGroup(func(g *Group) { 291 | for _, option := range g.options { 292 | f(c, g, option) 293 | } 294 | }) 295 | }, true) 296 | } 297 | 298 | func (c *Command) eachCommand(f func(*Command), recurse bool) { 299 | f(c) 300 | 301 | for _, cc := range c.commands { 302 | if recurse { 303 | cc.eachCommand(f, true) 304 | } else { 305 | f(cc) 306 | } 307 | } 308 | } 309 | 310 | func (c *Command) eachActiveGroup(f func(cc *Command, g *Group)) { 311 | c.eachGroup(func(g *Group) { 312 | f(c, g) 313 | }) 314 | 315 | if c.Active != nil { 316 | c.Active.eachActiveGroup(f) 317 | } 318 | } 319 | 320 | func (c *Command) addHelpGroups(showHelp func() error) { 321 | if !c.hasBuiltinHelpGroup { 322 | c.addHelpGroup(showHelp) 323 | c.hasBuiltinHelpGroup = true 324 | } 325 | 326 | for _, cc := range c.commands { 327 | cc.addHelpGroups(showHelp) 328 | } 329 | } 330 | 331 | func (c *Command) makeLookup() lookup { 332 | ret := lookup{ 333 | shortNames: make(map[string]*Option), 334 | longNames: make(map[string]*Option), 335 | commands: make(map[string]*Command), 336 | } 337 | 338 | parent := c.parent 339 | 340 | var parents []*Command 341 | 342 | for parent != nil { 343 | if cmd, ok := parent.(*Command); ok { 344 | parents = append(parents, cmd) 345 | parent = cmd.parent 346 | } else { 347 | parent = nil 348 | } 349 | } 350 | 351 | for i := len(parents) - 1; i >= 0; i-- { 352 | parents[i].fillLookup(&ret, true) 353 | } 354 | 355 | c.fillLookup(&ret, false) 356 | return ret 357 | } 358 | 359 | func (c *Command) fillLookup(ret *lookup, onlyOptions bool) { 360 | c.eachGroup(func(g *Group) { 361 | for _, option := range g.options { 362 | if option.ShortName != 0 { 363 | ret.shortNames[string(option.ShortName)] = option 364 | } 365 | 366 | if len(option.LongName) > 0 { 367 | ret.longNames[option.LongNameWithNamespace()] = option 368 | } 369 | } 370 | }) 371 | 372 | if onlyOptions { 373 | return 374 | } 375 | 376 | for _, subcommand := range c.commands { 377 | ret.commands[subcommand.Name] = subcommand 378 | 379 | for _, a := range subcommand.Aliases { 380 | ret.commands[a] = subcommand 381 | } 382 | } 383 | } 384 | 385 | func (c *Command) groupByName(name string) *Group { 386 | if grp := c.Group.groupByName(name); grp != nil { 387 | return grp 388 | } 389 | 390 | for _, subc := range c.commands { 391 | prefix := subc.Name + "." 392 | 393 | if strings.HasPrefix(name, prefix) { 394 | if grp := subc.groupByName(name[len(prefix):]); grp != nil { 395 | return grp 396 | } 397 | } else if name == subc.Name { 398 | return subc.Group 399 | } 400 | } 401 | 402 | return nil 403 | } 404 | 405 | type commandList []*Command 406 | 407 | func (c commandList) Less(i, j int) bool { 408 | return c[i].Name < c[j].Name 409 | } 410 | 411 | func (c commandList) Len() int { 412 | return len(c) 413 | } 414 | 415 | func (c commandList) Swap(i, j int) { 416 | c[i], c[j] = c[j], c[i] 417 | } 418 | 419 | func (c *Command) sortedVisibleCommands() []*Command { 420 | ret := commandList(c.visibleCommands()) 421 | sort.Sort(ret) 422 | 423 | return []*Command(ret) 424 | } 425 | 426 | func (c *Command) visibleCommands() []*Command { 427 | ret := make([]*Command, 0, len(c.commands)) 428 | 429 | for _, cmd := range c.commands { 430 | if !cmd.Hidden { 431 | ret = append(ret, cmd) 432 | } 433 | } 434 | 435 | return ret 436 | } 437 | 438 | func (c *Command) match(name string) bool { 439 | if c.Name == name { 440 | return true 441 | } 442 | 443 | for _, v := range c.Aliases { 444 | if v == name { 445 | return true 446 | } 447 | } 448 | 449 | return false 450 | } 451 | 452 | func (c *Command) hasHelpOptions() bool { 453 | ret := false 454 | 455 | c.eachGroup(func(g *Group) { 456 | if g.isBuiltinHelp { 457 | return 458 | } 459 | 460 | for _, opt := range g.options { 461 | if opt.showInHelp() { 462 | ret = true 463 | } 464 | } 465 | }) 466 | 467 | return ret 468 | } 469 | 470 | func (c *Command) fillParseState(s *parseState) { 471 | s.positional = make([]*Arg, len(c.args)) 472 | copy(s.positional, c.args) 473 | 474 | s.lookup = c.makeLookup() 475 | s.command = c 476 | } 477 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestCommandInline(t *testing.T) { 9 | var opts = struct { 10 | Value bool `short:"v"` 11 | 12 | Command struct { 13 | G bool `short:"g"` 14 | } `command:"cmd"` 15 | }{} 16 | 17 | p, ret := assertParserSuccess(t, &opts, "-v", "cmd", "-g") 18 | 19 | assertStringArray(t, ret, []string{}) 20 | 21 | if p.Active == nil { 22 | t.Errorf("Expected active command") 23 | } 24 | 25 | if !opts.Value { 26 | t.Errorf("Expected Value to be true") 27 | } 28 | 29 | if !opts.Command.G { 30 | t.Errorf("Expected Command.G to be true") 31 | } 32 | 33 | if p.Command.Find("cmd") != p.Active { 34 | t.Errorf("Expected to find command `cmd' to be active") 35 | } 36 | } 37 | 38 | func TestCommandInlineMulti(t *testing.T) { 39 | var opts = struct { 40 | Value bool `short:"v"` 41 | 42 | C1 struct { 43 | } `command:"c1"` 44 | 45 | C2 struct { 46 | G bool `short:"g"` 47 | } `command:"c2"` 48 | }{} 49 | 50 | p, ret := assertParserSuccess(t, &opts, "-v", "c2", "-g") 51 | 52 | assertStringArray(t, ret, []string{}) 53 | 54 | if p.Active == nil { 55 | t.Errorf("Expected active command") 56 | } 57 | 58 | if !opts.Value { 59 | t.Errorf("Expected Value to be true") 60 | } 61 | 62 | if !opts.C2.G { 63 | t.Errorf("Expected C2.G to be true") 64 | } 65 | 66 | if p.Command.Find("c1") == nil { 67 | t.Errorf("Expected to find command `c1'") 68 | } 69 | 70 | if c2 := p.Command.Find("c2"); c2 == nil { 71 | t.Errorf("Expected to find command `c2'") 72 | } else if c2 != p.Active { 73 | t.Errorf("Expected to find command `c2' to be active") 74 | } 75 | } 76 | 77 | func TestCommandFlagOrder1(t *testing.T) { 78 | var opts = struct { 79 | Value bool `short:"v"` 80 | 81 | Command struct { 82 | G bool `short:"g"` 83 | } `command:"cmd"` 84 | }{} 85 | 86 | assertParseFail(t, ErrUnknownFlag, "unknown flag `g'", &opts, "-v", "-g", "cmd") 87 | } 88 | 89 | func TestCommandFlagOrder2(t *testing.T) { 90 | var opts = struct { 91 | Value bool `short:"v"` 92 | 93 | Command struct { 94 | G bool `short:"g"` 95 | } `command:"cmd"` 96 | }{} 97 | 98 | assertParseSuccess(t, &opts, "cmd", "-v", "-g") 99 | 100 | if !opts.Value { 101 | t.Errorf("Expected Value to be true") 102 | } 103 | 104 | if !opts.Command.G { 105 | t.Errorf("Expected Command.G to be true") 106 | } 107 | } 108 | 109 | func TestCommandFlagOrderSub(t *testing.T) { 110 | var opts = struct { 111 | Value bool `short:"v"` 112 | 113 | Command struct { 114 | G bool `short:"g"` 115 | 116 | SubCommand struct { 117 | B bool `short:"b"` 118 | } `command:"sub"` 119 | } `command:"cmd"` 120 | }{} 121 | 122 | assertParseSuccess(t, &opts, "cmd", "sub", "-v", "-g", "-b") 123 | 124 | if !opts.Value { 125 | t.Errorf("Expected Value to be true") 126 | } 127 | 128 | if !opts.Command.G { 129 | t.Errorf("Expected Command.G to be true") 130 | } 131 | 132 | if !opts.Command.SubCommand.B { 133 | t.Errorf("Expected Command.SubCommand.B to be true") 134 | } 135 | } 136 | 137 | func TestCommandFlagOverride1(t *testing.T) { 138 | var opts = struct { 139 | Value bool `short:"v"` 140 | 141 | Command struct { 142 | Value bool `short:"v"` 143 | } `command:"cmd"` 144 | }{} 145 | 146 | assertParseSuccess(t, &opts, "-v", "cmd") 147 | 148 | if !opts.Value { 149 | t.Errorf("Expected Value to be true") 150 | } 151 | 152 | if opts.Command.Value { 153 | t.Errorf("Expected Command.Value to be false") 154 | } 155 | } 156 | 157 | func TestCommandFlagOverride2(t *testing.T) { 158 | var opts = struct { 159 | Value bool `short:"v"` 160 | 161 | Command struct { 162 | Value bool `short:"v"` 163 | } `command:"cmd"` 164 | }{} 165 | 166 | assertParseSuccess(t, &opts, "cmd", "-v") 167 | 168 | if opts.Value { 169 | t.Errorf("Expected Value to be false") 170 | } 171 | 172 | if !opts.Command.Value { 173 | t.Errorf("Expected Command.Value to be true") 174 | } 175 | } 176 | 177 | func TestCommandFlagOverrideSub(t *testing.T) { 178 | var opts = struct { 179 | Value bool `short:"v"` 180 | 181 | Command struct { 182 | Value bool `short:"v"` 183 | 184 | SubCommand struct { 185 | Value bool `short:"v"` 186 | } `command:"sub"` 187 | } `command:"cmd"` 188 | }{} 189 | 190 | assertParseSuccess(t, &opts, "cmd", "sub", "-v") 191 | 192 | if opts.Value { 193 | t.Errorf("Expected Value to be false") 194 | } 195 | 196 | if opts.Command.Value { 197 | t.Errorf("Expected Command.Value to be false") 198 | } 199 | 200 | if !opts.Command.SubCommand.Value { 201 | t.Errorf("Expected Command.Value to be true") 202 | } 203 | } 204 | 205 | func TestCommandFlagOverrideSub2(t *testing.T) { 206 | var opts = struct { 207 | Value bool `short:"v"` 208 | 209 | Command struct { 210 | Value bool `short:"v"` 211 | 212 | SubCommand struct { 213 | G bool `short:"g"` 214 | } `command:"sub"` 215 | } `command:"cmd"` 216 | }{} 217 | 218 | assertParseSuccess(t, &opts, "cmd", "sub", "-v") 219 | 220 | if opts.Value { 221 | t.Errorf("Expected Value to be false") 222 | } 223 | 224 | if !opts.Command.Value { 225 | t.Errorf("Expected Command.Value to be true") 226 | } 227 | } 228 | 229 | func TestCommandEstimate(t *testing.T) { 230 | var opts = struct { 231 | Value bool `short:"v"` 232 | 233 | Cmd1 struct { 234 | } `command:"remove"` 235 | 236 | Cmd2 struct { 237 | } `command:"add"` 238 | }{} 239 | 240 | p := NewParser(&opts, None) 241 | _, err := p.ParseArgs([]string{}) 242 | 243 | assertError(t, err, ErrCommandRequired, "Please specify one command of: add or remove") 244 | } 245 | 246 | func TestCommandEstimate2(t *testing.T) { 247 | var opts = struct { 248 | Value bool `short:"v"` 249 | 250 | Cmd1 struct { 251 | } `command:"remove"` 252 | 253 | Cmd2 struct { 254 | } `command:"add"` 255 | }{} 256 | 257 | p := NewParser(&opts, None) 258 | _, err := p.ParseArgs([]string{"rmive"}) 259 | 260 | assertError(t, err, ErrUnknownCommand, "Unknown command `rmive', did you mean `remove'?") 261 | } 262 | 263 | type testCommand struct { 264 | G bool `short:"g"` 265 | Executed bool 266 | EArgs []string 267 | } 268 | 269 | func (c *testCommand) Execute(args []string) error { 270 | c.Executed = true 271 | c.EArgs = args 272 | 273 | return nil 274 | } 275 | 276 | func TestCommandExecute(t *testing.T) { 277 | var opts = struct { 278 | Value bool `short:"v"` 279 | 280 | Command testCommand `command:"cmd"` 281 | }{} 282 | 283 | assertParseSuccess(t, &opts, "-v", "cmd", "-g", "a", "b") 284 | 285 | if !opts.Value { 286 | t.Errorf("Expected Value to be true") 287 | } 288 | 289 | if !opts.Command.Executed { 290 | t.Errorf("Did not execute command") 291 | } 292 | 293 | if !opts.Command.G { 294 | t.Errorf("Expected Command.C to be true") 295 | } 296 | 297 | assertStringArray(t, opts.Command.EArgs, []string{"a", "b"}) 298 | } 299 | 300 | func TestCommandClosest(t *testing.T) { 301 | var opts = struct { 302 | Value bool `short:"v"` 303 | 304 | Cmd1 struct { 305 | } `command:"remove"` 306 | 307 | Cmd2 struct { 308 | } `command:"add"` 309 | }{} 310 | 311 | args := assertParseFail(t, ErrUnknownCommand, "Unknown command `addd', did you mean `add'?", &opts, "-v", "addd") 312 | 313 | assertStringArray(t, args, []string{"addd"}) 314 | } 315 | 316 | func TestCommandAdd(t *testing.T) { 317 | var opts = struct { 318 | Value bool `short:"v"` 319 | }{} 320 | 321 | var cmd = struct { 322 | G bool `short:"g"` 323 | }{} 324 | 325 | p := NewParser(&opts, Default) 326 | c, err := p.AddCommand("cmd", "", "", &cmd) 327 | 328 | if err != nil { 329 | t.Fatalf("Unexpected error: %v", err) 330 | return 331 | } 332 | 333 | ret, err := p.ParseArgs([]string{"-v", "cmd", "-g", "rest"}) 334 | 335 | if err != nil { 336 | t.Fatalf("Unexpected error: %v", err) 337 | return 338 | } 339 | 340 | assertStringArray(t, ret, []string{"rest"}) 341 | 342 | if !opts.Value { 343 | t.Errorf("Expected Value to be true") 344 | } 345 | 346 | if !cmd.G { 347 | t.Errorf("Expected Command.G to be true") 348 | } 349 | 350 | if p.Command.Find("cmd") != c { 351 | t.Errorf("Expected to find command `cmd'") 352 | } 353 | 354 | if p.Commands()[0] != c { 355 | t.Errorf("Expected command %#v, but got %#v", c, p.Commands()[0]) 356 | } 357 | 358 | if c.Options()[0].ShortName != 'g' { 359 | t.Errorf("Expected short name `g' but got %v", c.Options()[0].ShortName) 360 | } 361 | } 362 | 363 | func TestCommandNestedInline(t *testing.T) { 364 | var opts = struct { 365 | Value bool `short:"v"` 366 | 367 | Command struct { 368 | G bool `short:"g"` 369 | 370 | Nested struct { 371 | N string `long:"n"` 372 | } `command:"nested"` 373 | } `command:"cmd"` 374 | }{} 375 | 376 | p, ret := assertParserSuccess(t, &opts, "-v", "cmd", "-g", "nested", "--n", "n", "rest") 377 | 378 | assertStringArray(t, ret, []string{"rest"}) 379 | 380 | if !opts.Value { 381 | t.Errorf("Expected Value to be true") 382 | } 383 | 384 | if !opts.Command.G { 385 | t.Errorf("Expected Command.G to be true") 386 | } 387 | 388 | assertString(t, opts.Command.Nested.N, "n") 389 | 390 | if c := p.Command.Find("cmd"); c == nil { 391 | t.Errorf("Expected to find command `cmd'") 392 | } else { 393 | if c != p.Active { 394 | t.Errorf("Expected `cmd' to be the active parser command") 395 | } 396 | 397 | if nested := c.Find("nested"); nested == nil { 398 | t.Errorf("Expected to find command `nested'") 399 | } else if nested != c.Active { 400 | t.Errorf("Expected to find command `nested' to be the active `cmd' command") 401 | } 402 | } 403 | } 404 | 405 | func TestRequiredOnCommand(t *testing.T) { 406 | var opts = struct { 407 | Value bool `short:"v" required:"true"` 408 | 409 | Command struct { 410 | G bool `short:"g"` 411 | } `command:"cmd"` 412 | }{} 413 | 414 | assertParseFail(t, ErrRequired, fmt.Sprintf("the required flag `%cv' was not specified", defaultShortOptDelimiter), &opts, "cmd") 415 | } 416 | 417 | func TestRequiredAllOnCommand(t *testing.T) { 418 | var opts = struct { 419 | Value bool `short:"v" required:"true"` 420 | Missing bool `long:"missing" required:"true"` 421 | 422 | Command struct { 423 | G bool `short:"g"` 424 | } `command:"cmd"` 425 | }{} 426 | 427 | assertParseFail(t, ErrRequired, fmt.Sprintf("the required flags `%smissing' and `%cv' were not specified", defaultLongOptDelimiter, defaultShortOptDelimiter), &opts, "cmd") 428 | } 429 | 430 | func TestDefaultOnCommand(t *testing.T) { 431 | var opts = struct { 432 | Command struct { 433 | G string `short:"g" default:"value"` 434 | } `command:"cmd"` 435 | }{} 436 | 437 | assertParseSuccess(t, &opts, "cmd") 438 | 439 | if opts.Command.G != "value" { 440 | t.Errorf("Expected G to be \"value\"") 441 | } 442 | } 443 | 444 | func TestAfterNonCommand(t *testing.T) { 445 | var opts = struct { 446 | Value bool `short:"v"` 447 | 448 | Cmd1 struct { 449 | } `command:"remove"` 450 | 451 | Cmd2 struct { 452 | } `command:"add"` 453 | }{} 454 | 455 | assertParseFail(t, ErrUnknownCommand, "Unknown command `nocmd'. Please specify one command of: add or remove", &opts, "nocmd", "remove") 456 | } 457 | 458 | func TestSubcommandsOptional(t *testing.T) { 459 | var opts = struct { 460 | Value bool `short:"v"` 461 | 462 | Cmd1 struct { 463 | } `command:"remove"` 464 | 465 | Cmd2 struct { 466 | } `command:"add"` 467 | }{} 468 | 469 | p := NewParser(&opts, None) 470 | p.SubcommandsOptional = true 471 | 472 | _, err := p.ParseArgs([]string{"-v"}) 473 | 474 | if err != nil { 475 | t.Fatalf("Unexpected error: %v", err) 476 | return 477 | } 478 | 479 | if !opts.Value { 480 | t.Errorf("Expected Value to be true") 481 | } 482 | } 483 | 484 | func TestSubcommandsOptionalAfterNonCommand(t *testing.T) { 485 | var opts = struct { 486 | Value bool `short:"v"` 487 | 488 | Cmd1 struct { 489 | } `command:"remove"` 490 | 491 | Cmd2 struct { 492 | } `command:"add"` 493 | }{} 494 | 495 | p := NewParser(&opts, None) 496 | p.SubcommandsOptional = true 497 | 498 | retargs, err := p.ParseArgs([]string{"nocmd", "remove"}) 499 | 500 | if err != nil { 501 | t.Fatalf("Unexpected error: %v", err) 502 | return 503 | } 504 | 505 | assertStringArray(t, retargs, []string{"nocmd", "remove"}) 506 | } 507 | 508 | func TestCommandAlias(t *testing.T) { 509 | var opts = struct { 510 | Command struct { 511 | G string `short:"g" default:"value"` 512 | } `command:"cmd" alias:"cm"` 513 | }{} 514 | 515 | assertParseSuccess(t, &opts, "cm") 516 | 517 | if opts.Command.G != "value" { 518 | t.Errorf("Expected G to be \"value\"") 519 | } 520 | } 521 | 522 | func TestSubCommandFindOptionByLongFlag(t *testing.T) { 523 | var opts struct { 524 | Testing bool `long:"testing" description:"Testing"` 525 | } 526 | 527 | var cmd struct { 528 | Other bool `long:"other" description:"Other"` 529 | } 530 | 531 | p := NewParser(&opts, Default) 532 | c, _ := p.AddCommand("command", "Short", "Long", &cmd) 533 | 534 | opt := c.FindOptionByLongName("other") 535 | 536 | if opt == nil { 537 | t.Errorf("Expected option, but found none") 538 | } 539 | 540 | assertString(t, opt.LongName, "other") 541 | 542 | opt = c.FindOptionByLongName("testing") 543 | 544 | if opt == nil { 545 | t.Errorf("Expected option, but found none") 546 | } 547 | 548 | assertString(t, opt.LongName, "testing") 549 | } 550 | 551 | func TestSubCommandFindOptionByShortFlag(t *testing.T) { 552 | var opts struct { 553 | Testing bool `short:"t" description:"Testing"` 554 | } 555 | 556 | var cmd struct { 557 | Other bool `short:"o" description:"Other"` 558 | } 559 | 560 | p := NewParser(&opts, Default) 561 | c, _ := p.AddCommand("command", "Short", "Long", &cmd) 562 | 563 | opt := c.FindOptionByShortName('o') 564 | 565 | if opt == nil { 566 | t.Errorf("Expected option, but found none") 567 | } 568 | 569 | if opt.ShortName != 'o' { 570 | t.Errorf("Expected 'o', but got %v", opt.ShortName) 571 | } 572 | 573 | opt = c.FindOptionByShortName('t') 574 | 575 | if opt == nil { 576 | t.Errorf("Expected option, but found none") 577 | } 578 | 579 | if opt.ShortName != 't' { 580 | t.Errorf("Expected 'o', but got %v", opt.ShortName) 581 | } 582 | } 583 | 584 | type fooCmd struct { 585 | Flag bool `short:"f"` 586 | args []string 587 | } 588 | 589 | func (foo *fooCmd) Execute(s []string) error { 590 | foo.args = s 591 | return nil 592 | } 593 | 594 | func TestCommandPassAfterNonOption(t *testing.T) { 595 | var opts = struct { 596 | Value bool `short:"v"` 597 | Foo fooCmd `command:"foo"` 598 | }{} 599 | p := NewParser(&opts, PassAfterNonOption) 600 | ret, err := p.ParseArgs([]string{"-v", "foo", "-f", "bar", "-v", "-g"}) 601 | 602 | if err != nil { 603 | t.Fatalf("Unexpected error: %v", err) 604 | return 605 | } 606 | 607 | if !opts.Value { 608 | t.Errorf("Expected Value to be true") 609 | } 610 | 611 | if !opts.Foo.Flag { 612 | t.Errorf("Expected Foo.Flag to be true") 613 | } 614 | 615 | assertStringArray(t, ret, []string{"bar", "-v", "-g"}) 616 | assertStringArray(t, opts.Foo.args, []string{"bar", "-v", "-g"}) 617 | } 618 | 619 | type barCmd struct { 620 | fooCmd 621 | Positional struct { 622 | Args []string 623 | } `positional-args:"yes"` 624 | } 625 | 626 | func TestCommandPassAfterNonOptionWithPositional(t *testing.T) { 627 | var opts = struct { 628 | Value bool `short:"v"` 629 | Bar barCmd `command:"bar"` 630 | }{} 631 | p := NewParser(&opts, PassAfterNonOption) 632 | ret, err := p.ParseArgs([]string{"-v", "bar", "-f", "baz", "-v", "-g"}) 633 | 634 | if err != nil { 635 | t.Fatalf("Unexpected error: %v", err) 636 | return 637 | } 638 | 639 | if !opts.Value { 640 | t.Errorf("Expected Value to be true") 641 | } 642 | 643 | if !opts.Bar.Flag { 644 | t.Errorf("Expected Bar.Flag to be true") 645 | } 646 | 647 | assertStringArray(t, ret, []string{}) 648 | assertStringArray(t, opts.Bar.args, []string{}) 649 | assertStringArray(t, opts.Bar.Positional.Args, []string{"baz", "-v", "-g"}) 650 | } 651 | 652 | type cmdLocalPassAfterNonOptionMix struct { 653 | FlagA bool `short:"a"` 654 | Cmd1 struct { 655 | FlagB bool `short:"b"` 656 | Positional struct { 657 | Args []string 658 | } `positional-args:"yes"` 659 | } `command:"cmd1" pass-after-non-option:"yes"` 660 | Cmd2 struct { 661 | FlagB bool `short:"b"` 662 | Positional struct { 663 | Args []string 664 | } `positional-args:"yes"` 665 | } `command:"cmd2"` 666 | } 667 | 668 | func TestCommandLocalPassAfterNonOptionMixCmd1(t *testing.T) { 669 | var opts cmdLocalPassAfterNonOptionMix 670 | 671 | assertParseSuccess(t, &opts, "cmd1", "-b", "arg1", "-a", "arg2", "-x") 672 | 673 | if opts.FlagA { 674 | t.Errorf("Expected FlagA to be false") 675 | } 676 | 677 | if !opts.Cmd1.FlagB { 678 | t.Errorf("Expected Cmd1.FlagB to be true") 679 | } 680 | 681 | assertStringArray(t, opts.Cmd1.Positional.Args, []string{"arg1", "-a", "arg2", "-x"}) 682 | } 683 | 684 | func TestCommandLocalPassAfterNonOptionMixCmd2(t *testing.T) { 685 | var opts cmdLocalPassAfterNonOptionMix 686 | 687 | assertParseSuccess(t, &opts, "cmd2", "-b", "arg1", "-a", "arg2") 688 | 689 | if !opts.FlagA { 690 | t.Errorf("Expected FlagA to be true") 691 | } 692 | 693 | if !opts.Cmd2.FlagB { 694 | t.Errorf("Expected Cmd2.FlagB to be true") 695 | } 696 | 697 | assertStringArray(t, opts.Cmd2.Positional.Args, []string{"arg1", "arg2"}) 698 | } 699 | 700 | func TestCommandLocalPassAfterNonOptionMixCmd2UnkownFlag(t *testing.T) { 701 | var opts cmdLocalPassAfterNonOptionMix 702 | 703 | assertParseFail(t, ErrUnknownFlag, "unknown flag `x'", &opts, "cmd2", "-b", "arg1", "-a", "arg2", "-x") 704 | } 705 | 706 | type cmdLocalPassAfterNonOptionNest struct { 707 | FlagA bool `short:"a"` 708 | Cmd1 struct { 709 | FlagB bool `short:"b"` 710 | Cmd2 struct { 711 | FlagC bool `short:"c"` 712 | Cmd3 struct { 713 | FlagD bool `short:"d"` 714 | } `command:"cmd3"` 715 | } `command:"cmd2" subcommands-optional:"yes" pass-after-non-option:"yes"` 716 | } `command:"cmd1"` 717 | } 718 | 719 | func TestCommandLocalPassAfterNonOptionNest1(t *testing.T) { 720 | var opts cmdLocalPassAfterNonOptionNest 721 | 722 | ret := assertParseSuccess(t, &opts, "cmd1", "cmd2", "-a", "x", "-b", "cmd3", "-c", "-d") 723 | 724 | if !opts.FlagA { 725 | t.Errorf("Expected FlagA to be true") 726 | } 727 | 728 | if opts.Cmd1.FlagB { 729 | t.Errorf("Expected Cmd1.FlagB to be false") 730 | } 731 | 732 | if opts.Cmd1.Cmd2.FlagC { 733 | t.Errorf("Expected Cmd1.Cmd2.FlagC to be false") 734 | } 735 | 736 | if opts.Cmd1.Cmd2.Cmd3.FlagD { 737 | t.Errorf("Expected Cmd1.Cmd2.Cmd3.FlagD to be false") 738 | } 739 | 740 | assertStringArray(t, ret, []string{"x", "-b", "cmd3", "-c", "-d"}) 741 | } 742 | 743 | func TestCommandLocalPassAfterNonOptionNest2(t *testing.T) { 744 | var opts cmdLocalPassAfterNonOptionNest 745 | 746 | ret := assertParseSuccess(t, &opts, "cmd1", "cmd2", "cmd3", "-a", "x", "-b", "-c", "-d") 747 | 748 | if !opts.FlagA { 749 | t.Errorf("Expected FlagA to be true") 750 | } 751 | 752 | if !opts.Cmd1.FlagB { 753 | t.Errorf("Expected Cmd1.FlagB to be true") 754 | } 755 | 756 | if !opts.Cmd1.Cmd2.FlagC { 757 | t.Errorf("Expected Cmd1.Cmd2.FlagC to be true") 758 | } 759 | 760 | if !opts.Cmd1.Cmd2.Cmd3.FlagD { 761 | t.Errorf("Expected Cmd1.Cmd2.Cmd3.FlagD to be true") 762 | } 763 | 764 | assertStringArray(t, ret, []string{"x"}) 765 | } 766 | -------------------------------------------------------------------------------- /completion.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "reflect" 8 | "sort" 9 | "strings" 10 | "unicode/utf8" 11 | ) 12 | 13 | // Completion is a type containing information of a completion. 14 | type Completion struct { 15 | // The completed item 16 | Item string 17 | 18 | // A description of the completed item (optional) 19 | Description string 20 | } 21 | 22 | type completions []Completion 23 | 24 | func (c completions) Len() int { 25 | return len(c) 26 | } 27 | 28 | func (c completions) Less(i, j int) bool { 29 | return c[i].Item < c[j].Item 30 | } 31 | 32 | func (c completions) Swap(i, j int) { 33 | c[i], c[j] = c[j], c[i] 34 | } 35 | 36 | // Completer is an interface which can be implemented by types 37 | // to provide custom command line argument completion. 38 | type Completer interface { 39 | // Complete receives a prefix representing a (partial) value 40 | // for its type and should provide a list of possible valid 41 | // completions. 42 | Complete(match string) []Completion 43 | } 44 | 45 | type completion struct { 46 | parser *Parser 47 | } 48 | 49 | // Filename is a string alias which provides filename completion. 50 | type Filename string 51 | 52 | func completionsWithoutDescriptions(items []string) []Completion { 53 | ret := make([]Completion, len(items)) 54 | 55 | for i, v := range items { 56 | ret[i].Item = v 57 | } 58 | 59 | return ret 60 | } 61 | 62 | // Complete returns a list of existing files with the given 63 | // prefix. 64 | func (f *Filename) Complete(match string) []Completion { 65 | ret, _ := filepath.Glob(match + "*") 66 | if len(ret) == 1 { 67 | if info, err := os.Stat(ret[0]); err == nil && info.IsDir() { 68 | ret[0] = ret[0] + "/" 69 | } 70 | } 71 | return completionsWithoutDescriptions(ret) 72 | } 73 | 74 | func (c *completion) skipPositional(s *parseState, n int) { 75 | if n >= len(s.positional) { 76 | s.positional = nil 77 | } else { 78 | s.positional = s.positional[n:] 79 | } 80 | } 81 | 82 | func (c *completion) completeOptionNames(s *parseState, prefix string, match string, short bool) []Completion { 83 | if short && len(match) != 0 { 84 | return []Completion{ 85 | { 86 | Item: prefix + match, 87 | }, 88 | } 89 | } 90 | 91 | var results []Completion 92 | repeats := map[string]bool{} 93 | 94 | var longprefix string 95 | var shortprefix string 96 | 97 | if prefix == "/" { 98 | longprefix = "/" 99 | shortprefix = "/" 100 | } else { 101 | longprefix = "--" 102 | shortprefix = "-" 103 | } 104 | 105 | for name, opt := range s.lookup.longNames { 106 | if strings.HasPrefix(name, match) && !opt.Hidden { 107 | results = append(results, Completion{ 108 | Item: longprefix + name, 109 | Description: opt.Description, 110 | }) 111 | 112 | if short { 113 | repeats[string(opt.ShortName)] = true 114 | } 115 | } 116 | } 117 | 118 | if short { 119 | for name, opt := range s.lookup.shortNames { 120 | if _, exist := repeats[name]; !exist && strings.HasPrefix(name, match) && !opt.Hidden { 121 | results = append(results, Completion{ 122 | Item: shortprefix + name, 123 | Description: opt.Description, 124 | }) 125 | } 126 | } 127 | } 128 | 129 | return results 130 | } 131 | 132 | func (c *completion) completeNamesForLongPrefix(s *parseState, prefix string, match string) []Completion { 133 | return c.completeOptionNames(s, prefix, match, false) 134 | } 135 | 136 | func (c *completion) completeNamesForShortPrefix(s *parseState, prefix string, match string) []Completion { 137 | return c.completeOptionNames(s, prefix, match, true) 138 | } 139 | 140 | func (c *completion) completeCommands(s *parseState, match string) []Completion { 141 | n := make([]Completion, 0, len(s.command.commands)) 142 | 143 | for _, cmd := range s.command.commands { 144 | if cmd.data != c && !cmd.Hidden && strings.HasPrefix(cmd.Name, match) { 145 | n = append(n, Completion{ 146 | Item: cmd.Name, 147 | Description: cmd.ShortDescription, 148 | }) 149 | } 150 | } 151 | 152 | return n 153 | } 154 | 155 | func (c *completion) completeValue(value reflect.Value, prefix string, match string) []Completion { 156 | if value.Kind() == reflect.Slice { 157 | value = reflect.New(value.Type().Elem()) 158 | } 159 | i := value.Interface() 160 | 161 | var ret []Completion 162 | 163 | if cmp, ok := i.(Completer); ok { 164 | ret = cmp.Complete(match) 165 | } else if value.CanAddr() { 166 | if cmp, ok = value.Addr().Interface().(Completer); ok { 167 | ret = cmp.Complete(match) 168 | } 169 | } 170 | 171 | for i, v := range ret { 172 | ret[i].Item = prefix + v.Item 173 | } 174 | 175 | return ret 176 | } 177 | 178 | func (c *completion) complete(args []string) []Completion { 179 | if len(args) == 0 { 180 | args = []string{""} 181 | } 182 | 183 | s := &parseState{ 184 | args: args, 185 | } 186 | 187 | c.parser.fillParseState(s) 188 | 189 | var opt *Option 190 | 191 | for len(s.args) > 1 { 192 | arg := s.pop() 193 | 194 | if (c.parser.Options&PassDoubleDash) != None && arg == "--" { 195 | opt = nil 196 | c.skipPositional(s, len(s.args)-1) 197 | 198 | break 199 | } 200 | 201 | if argumentIsOption(arg) { 202 | prefix, optname, islong := stripOptionPrefix(arg) 203 | optname, _, argument := splitOption(prefix, optname, islong) 204 | 205 | if argument == nil { 206 | var o *Option 207 | canarg := true 208 | 209 | if islong { 210 | o = s.lookup.longNames[optname] 211 | } else { 212 | for i, r := range optname { 213 | sname := string(r) 214 | o = s.lookup.shortNames[sname] 215 | 216 | if o == nil { 217 | break 218 | } 219 | 220 | if i == 0 && o.canArgument() && len(optname) != len(sname) { 221 | canarg = false 222 | break 223 | } 224 | } 225 | } 226 | 227 | if o == nil && (c.parser.Options&PassAfterNonOption) != None { 228 | opt = nil 229 | c.skipPositional(s, len(s.args)-1) 230 | 231 | break 232 | } else if o != nil && o.canArgument() && !o.OptionalArgument && canarg { 233 | if len(s.args) > 1 { 234 | s.pop() 235 | } else { 236 | opt = o 237 | } 238 | } 239 | } 240 | } else { 241 | if len(s.positional) > 0 { 242 | if !s.positional[0].isRemaining() { 243 | // Don't advance beyond a remaining positional arg (because 244 | // it consumes all subsequent args). 245 | s.positional = s.positional[1:] 246 | } 247 | } else if cmd, ok := s.lookup.commands[arg]; ok { 248 | cmd.fillParseState(s) 249 | } 250 | 251 | opt = nil 252 | } 253 | } 254 | 255 | lastarg := s.args[len(s.args)-1] 256 | var ret []Completion 257 | 258 | if opt != nil { 259 | // Completion for the argument of 'opt' 260 | ret = c.completeValue(opt.value, "", lastarg) 261 | } else if argumentStartsOption(lastarg) { 262 | // Complete the option 263 | prefix, optname, islong := stripOptionPrefix(lastarg) 264 | optname, split, argument := splitOption(prefix, optname, islong) 265 | 266 | if argument == nil && !islong { 267 | rname, n := utf8.DecodeRuneInString(optname) 268 | sname := string(rname) 269 | 270 | if opt := s.lookup.shortNames[sname]; opt != nil && opt.canArgument() { 271 | ret = c.completeValue(opt.value, prefix+sname, optname[n:]) 272 | } else { 273 | ret = c.completeNamesForShortPrefix(s, prefix, optname) 274 | } 275 | } else if argument != nil { 276 | if islong { 277 | opt = s.lookup.longNames[optname] 278 | } else { 279 | opt = s.lookup.shortNames[optname] 280 | } 281 | 282 | if opt != nil { 283 | ret = c.completeValue(opt.value, prefix+optname+split, *argument) 284 | } 285 | } else if islong { 286 | ret = c.completeNamesForLongPrefix(s, prefix, optname) 287 | } else { 288 | ret = c.completeNamesForShortPrefix(s, prefix, optname) 289 | } 290 | } else if len(s.positional) > 0 { 291 | // Complete for positional argument 292 | ret = c.completeValue(s.positional[0].value, "", lastarg) 293 | } else if len(s.command.commands) > 0 { 294 | // Complete for command 295 | ret = c.completeCommands(s, lastarg) 296 | } 297 | 298 | sort.Sort(completions(ret)) 299 | return ret 300 | } 301 | 302 | func (c *completion) print(items []Completion, showDescriptions bool) { 303 | if showDescriptions && len(items) > 1 { 304 | maxl := 0 305 | 306 | for _, v := range items { 307 | if len(v.Item) > maxl { 308 | maxl = len(v.Item) 309 | } 310 | } 311 | 312 | for _, v := range items { 313 | fmt.Printf("%s", v.Item) 314 | 315 | if len(v.Description) > 0 { 316 | fmt.Printf("%s # %s", strings.Repeat(" ", maxl-len(v.Item)), v.Description) 317 | } 318 | 319 | fmt.Printf("\n") 320 | } 321 | } else { 322 | for _, v := range items { 323 | fmt.Println(v.Item) 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /completion_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path" 8 | "path/filepath" 9 | "reflect" 10 | "runtime" 11 | "sort" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | type TestComplete struct { 17 | } 18 | 19 | func (t *TestComplete) Complete(match string) []Completion { 20 | options := []string{ 21 | "hello world", 22 | "hello universe", 23 | "hello multiverse", 24 | } 25 | 26 | ret := make([]Completion, 0, len(options)) 27 | 28 | for _, o := range options { 29 | if strings.HasPrefix(o, match) { 30 | ret = append(ret, Completion{ 31 | Item: o, 32 | }) 33 | } 34 | } 35 | 36 | return ret 37 | } 38 | 39 | var completionTestOptions struct { 40 | Verbose bool `short:"v" long:"verbose" description:"Verbose messages"` 41 | Debug bool `short:"d" long:"debug" description:"Enable debug"` 42 | Info bool `short:"i" description:"Display info"` 43 | Version bool `long:"version" description:"Show version"` 44 | Required bool `long:"required" required:"true" description:"This is required"` 45 | Hidden bool `long:"hidden" hidden:"true" description:"This is hidden"` 46 | 47 | AddCommand struct { 48 | Positional struct { 49 | Filename Filename 50 | } `positional-args:"yes"` 51 | } `command:"add" description:"add an item"` 52 | 53 | AddMultiCommand struct { 54 | Positional struct { 55 | Filename []Filename 56 | } `positional-args:"yes"` 57 | Extra []Filename `short:"f"` 58 | } `command:"add-multi" description:"add multiple items"` 59 | 60 | AddMultiCommandFlag struct { 61 | Files []Filename `short:"f"` 62 | } `command:"add-multi-flag" description:"add multiple items via flags"` 63 | 64 | RemoveCommand struct { 65 | Other bool `short:"o"` 66 | File Filename `short:"f" long:"filename"` 67 | } `command:"rm" description:"remove an item"` 68 | 69 | RenameCommand struct { 70 | Completed TestComplete `short:"c" long:"completed"` 71 | } `command:"rename" description:"rename an item"` 72 | 73 | HiddenCommand struct { 74 | } `command:"hidden" description:"hidden command" hidden:"true"` 75 | } 76 | 77 | type completionTest struct { 78 | Args []string 79 | Completed []string 80 | ShowDescriptions bool 81 | } 82 | 83 | var completionTests []completionTest 84 | 85 | func makeLongName(option string) string { 86 | return defaultLongOptDelimiter + option 87 | } 88 | 89 | func makeShortName(option string) string { 90 | return string(defaultShortOptDelimiter) + option 91 | } 92 | 93 | func init() { 94 | _, sourcefile, _, _ := runtime.Caller(0) 95 | completionTestSourcedir := filepath.Join(filepath.SplitList(path.Dir(sourcefile))...) 96 | 97 | completionTestFilename := []string{filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion_test.go")} 98 | 99 | completionTestSubdir := []string{ 100 | filepath.Join(completionTestSourcedir, "examples/add.go"), 101 | filepath.Join(completionTestSourcedir, "examples/bash-completion"), 102 | filepath.Join(completionTestSourcedir, "examples/main.go"), 103 | filepath.Join(completionTestSourcedir, "examples/rm.go"), 104 | } 105 | 106 | completionTests = []completionTest{ 107 | { 108 | // Short names 109 | []string{makeShortName("")}, 110 | []string{makeLongName("debug"), makeLongName("required"), makeLongName("verbose"), makeLongName("version"), makeShortName("i")}, 111 | false, 112 | }, 113 | 114 | { 115 | // Short names full 116 | []string{makeShortName("i")}, 117 | []string{makeShortName("i")}, 118 | false, 119 | }, 120 | 121 | { 122 | // Short names concatenated 123 | []string{"-dv"}, 124 | []string{"-dv"}, 125 | false, 126 | }, 127 | 128 | { 129 | // Long names 130 | []string{"--"}, 131 | []string{"--debug", "--required", "--verbose", "--version"}, 132 | false, 133 | }, 134 | 135 | { 136 | // Long names with descriptions 137 | []string{"--"}, 138 | []string{ 139 | "--debug # Enable debug", 140 | "--required # This is required", 141 | "--verbose # Verbose messages", 142 | "--version # Show version", 143 | }, 144 | true, 145 | }, 146 | 147 | { 148 | // Long names partial 149 | []string{makeLongName("ver")}, 150 | []string{makeLongName("verbose"), makeLongName("version")}, 151 | false, 152 | }, 153 | 154 | { 155 | // Commands 156 | []string{""}, 157 | []string{"add", "add-multi", "add-multi-flag", "rename", "rm"}, 158 | false, 159 | }, 160 | 161 | { 162 | // Commands with descriptions 163 | []string{""}, 164 | []string{ 165 | "add # add an item", 166 | "add-multi # add multiple items", 167 | "add-multi-flag # add multiple items via flags", 168 | "rename # rename an item", 169 | "rm # remove an item", 170 | }, 171 | true, 172 | }, 173 | 174 | { 175 | // Commands partial 176 | []string{"r"}, 177 | []string{"rename", "rm"}, 178 | false, 179 | }, 180 | 181 | { 182 | // Positional filename 183 | []string{"add", filepath.Join(completionTestSourcedir, "completion")}, 184 | completionTestFilename, 185 | false, 186 | }, 187 | 188 | { 189 | // Multiple positional filename (1 arg) 190 | []string{"add-multi", filepath.Join(completionTestSourcedir, "completion")}, 191 | completionTestFilename, 192 | false, 193 | }, 194 | { 195 | // Multiple positional filename (2 args) 196 | []string{"add-multi", filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion")}, 197 | completionTestFilename, 198 | false, 199 | }, 200 | { 201 | // Multiple positional filename (3 args) 202 | []string{"add-multi", filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion")}, 203 | completionTestFilename, 204 | false, 205 | }, 206 | 207 | { 208 | // Flag filename 209 | []string{"rm", makeShortName("f"), filepath.Join(completionTestSourcedir, "completion")}, 210 | completionTestFilename, 211 | false, 212 | }, 213 | 214 | { 215 | // Flag short concat last filename 216 | []string{"rm", "-of", filepath.Join(completionTestSourcedir, "completion")}, 217 | completionTestFilename, 218 | false, 219 | }, 220 | 221 | { 222 | // Flag concat filename 223 | []string{"rm", "-f" + filepath.Join(completionTestSourcedir, "completion")}, 224 | []string{"-f" + completionTestFilename[0], "-f" + completionTestFilename[1]}, 225 | false, 226 | }, 227 | 228 | { 229 | // Flag equal concat filename 230 | []string{"rm", "-f=" + filepath.Join(completionTestSourcedir, "completion")}, 231 | []string{"-f=" + completionTestFilename[0], "-f=" + completionTestFilename[1]}, 232 | false, 233 | }, 234 | 235 | { 236 | // Flag concat long filename 237 | []string{"rm", "--filename=" + filepath.Join(completionTestSourcedir, "completion")}, 238 | []string{"--filename=" + completionTestFilename[0], "--filename=" + completionTestFilename[1]}, 239 | false, 240 | }, 241 | 242 | { 243 | // Flag long filename 244 | []string{"rm", "--filename", filepath.Join(completionTestSourcedir, "completion")}, 245 | completionTestFilename, 246 | false, 247 | }, 248 | 249 | { 250 | // To subdir 251 | []string{"rm", "--filename", filepath.Join(completionTestSourcedir, "examples/bash-")}, 252 | []string{filepath.Join(completionTestSourcedir, "examples/bash-completion/")}, 253 | false, 254 | }, 255 | 256 | { 257 | // Subdirectory 258 | []string{"rm", "--filename", filepath.Join(completionTestSourcedir, "examples") + "/"}, 259 | completionTestSubdir, 260 | false, 261 | }, 262 | 263 | { 264 | // Custom completed 265 | []string{"rename", makeShortName("c"), "hello un"}, 266 | []string{"hello universe"}, 267 | false, 268 | }, 269 | { 270 | // Multiple flag filename 271 | []string{"add-multi-flag", makeShortName("f"), filepath.Join(completionTestSourcedir, "completion")}, 272 | completionTestFilename, 273 | false, 274 | }, 275 | } 276 | } 277 | 278 | func TestCompletion(t *testing.T) { 279 | p := NewParser(&completionTestOptions, Default) 280 | c := &completion{parser: p} 281 | 282 | for _, test := range completionTests { 283 | if test.ShowDescriptions { 284 | continue 285 | } 286 | 287 | ret := c.complete(test.Args) 288 | items := make([]string, len(ret)) 289 | 290 | for i, v := range ret { 291 | items[i] = v.Item 292 | } 293 | 294 | sort.Strings(items) 295 | sort.Strings(test.Completed) 296 | 297 | if !reflect.DeepEqual(items, test.Completed) { 298 | t.Errorf("Args: %#v, %#v\n Expected: %#v\n Got: %#v", test.Args, test.ShowDescriptions, test.Completed, items) 299 | } 300 | } 301 | } 302 | 303 | func TestParserCompletion(t *testing.T) { 304 | for _, test := range completionTests { 305 | if test.ShowDescriptions { 306 | os.Setenv("GO_FLAGS_COMPLETION", "verbose") 307 | } else { 308 | os.Setenv("GO_FLAGS_COMPLETION", "1") 309 | } 310 | 311 | tmp := os.Stdout 312 | 313 | r, w, _ := os.Pipe() 314 | os.Stdout = w 315 | 316 | out := make(chan string) 317 | 318 | go func() { 319 | var buf bytes.Buffer 320 | 321 | io.Copy(&buf, r) 322 | 323 | out <- buf.String() 324 | }() 325 | 326 | p := NewParser(&completionTestOptions, None) 327 | 328 | p.CompletionHandler = func(items []Completion) { 329 | comp := &completion{parser: p} 330 | comp.print(items, test.ShowDescriptions) 331 | } 332 | 333 | _, err := p.ParseArgs(test.Args) 334 | 335 | w.Close() 336 | 337 | os.Stdout = tmp 338 | 339 | if err != nil { 340 | t.Fatalf("Unexpected error: %s", err) 341 | } 342 | 343 | got := strings.Split(strings.Trim(<-out, "\n"), "\n") 344 | 345 | if !reflect.DeepEqual(got, test.Completed) { 346 | t.Errorf("Expected: %#v\nGot: %#v", test.Completed, got) 347 | } 348 | } 349 | 350 | os.Setenv("GO_FLAGS_COMPLETION", "") 351 | } 352 | -------------------------------------------------------------------------------- /convert.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Jesse van den Kieboom. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package flags 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // Marshaler is the interface implemented by types that can marshal themselves 16 | // to a string representation of the flag. 17 | type Marshaler interface { 18 | // MarshalFlag marshals a flag value to its string representation. 19 | MarshalFlag() (string, error) 20 | } 21 | 22 | // Unmarshaler is the interface implemented by types that can unmarshal a flag 23 | // argument to themselves. The provided value is directly passed from the 24 | // command line. 25 | type Unmarshaler interface { 26 | // UnmarshalFlag unmarshals a string value representation to the flag 27 | // value (which therefore needs to be a pointer receiver). 28 | UnmarshalFlag(value string) error 29 | } 30 | 31 | // ValueValidator is the interface implemented by types that can validate a 32 | // flag argument themselves. The provided value is directly passed from the 33 | // command line. 34 | type ValueValidator interface { 35 | // IsValidValue returns an error if the provided string value is valid for 36 | // the flag. 37 | IsValidValue(value string) error 38 | } 39 | 40 | func getBase(options multiTag, base int) (int, error) { 41 | sbase := options.Get("base") 42 | 43 | var err error 44 | var ivbase int64 45 | 46 | if sbase != "" { 47 | ivbase, err = strconv.ParseInt(sbase, 10, 32) 48 | base = int(ivbase) 49 | } 50 | 51 | return base, err 52 | } 53 | 54 | func convertMarshal(val reflect.Value) (bool, string, error) { 55 | // Check first for the Marshaler interface 56 | if val.IsValid() && val.Type().NumMethod() > 0 && val.CanInterface() { 57 | if marshaler, ok := val.Interface().(Marshaler); ok { 58 | ret, err := marshaler.MarshalFlag() 59 | return true, ret, err 60 | } 61 | } 62 | 63 | return false, "", nil 64 | } 65 | 66 | func convertToString(val reflect.Value, options multiTag) (string, error) { 67 | if ok, ret, err := convertMarshal(val); ok { 68 | return ret, err 69 | } 70 | 71 | if !val.IsValid() { 72 | return "", nil 73 | } 74 | 75 | tp := val.Type() 76 | 77 | // Support for time.Duration 78 | if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() { 79 | stringer := val.Interface().(fmt.Stringer) 80 | return stringer.String(), nil 81 | } 82 | 83 | switch tp.Kind() { 84 | case reflect.String: 85 | return val.String(), nil 86 | case reflect.Bool: 87 | if val.Bool() { 88 | return "true", nil 89 | } 90 | 91 | return "false", nil 92 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 93 | base, err := getBase(options, 10) 94 | 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | return strconv.FormatInt(val.Int(), base), nil 100 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 101 | base, err := getBase(options, 10) 102 | 103 | if err != nil { 104 | return "", err 105 | } 106 | 107 | return strconv.FormatUint(val.Uint(), base), nil 108 | case reflect.Float32, reflect.Float64: 109 | return strconv.FormatFloat(val.Float(), 'g', -1, tp.Bits()), nil 110 | case reflect.Slice: 111 | if val.Len() == 0 { 112 | return "", nil 113 | } 114 | 115 | ret := "[" 116 | 117 | for i := 0; i < val.Len(); i++ { 118 | if i != 0 { 119 | ret += ", " 120 | } 121 | 122 | item, err := convertToString(val.Index(i), options) 123 | 124 | if err != nil { 125 | return "", err 126 | } 127 | 128 | ret += item 129 | } 130 | 131 | return ret + "]", nil 132 | case reflect.Map: 133 | ret := "{" 134 | 135 | for i, key := range val.MapKeys() { 136 | if i != 0 { 137 | ret += ", " 138 | } 139 | 140 | keyitem, err := convertToString(key, options) 141 | 142 | if err != nil { 143 | return "", err 144 | } 145 | 146 | item, err := convertToString(val.MapIndex(key), options) 147 | 148 | if err != nil { 149 | return "", err 150 | } 151 | 152 | ret += keyitem + ":" + item 153 | } 154 | 155 | return ret + "}", nil 156 | case reflect.Ptr: 157 | return convertToString(reflect.Indirect(val), options) 158 | case reflect.Interface: 159 | if !val.IsNil() { 160 | return convertToString(val.Elem(), options) 161 | } 162 | } 163 | 164 | return "", nil 165 | } 166 | 167 | func convertUnmarshal(val string, retval reflect.Value) (bool, error) { 168 | if retval.Type().NumMethod() > 0 && retval.CanInterface() { 169 | if unmarshaler, ok := retval.Interface().(Unmarshaler); ok { 170 | if retval.IsNil() { 171 | retval.Set(reflect.New(retval.Type().Elem())) 172 | 173 | // Re-assign from the new value 174 | unmarshaler = retval.Interface().(Unmarshaler) 175 | } 176 | 177 | return true, unmarshaler.UnmarshalFlag(val) 178 | } 179 | } 180 | 181 | if retval.Type().Kind() != reflect.Ptr && retval.CanAddr() { 182 | return convertUnmarshal(val, retval.Addr()) 183 | } 184 | 185 | if retval.Type().Kind() == reflect.Interface && !retval.IsNil() { 186 | return convertUnmarshal(val, retval.Elem()) 187 | } 188 | 189 | return false, nil 190 | } 191 | 192 | func convert(val string, retval reflect.Value, options multiTag) error { 193 | if ok, err := convertUnmarshal(val, retval); ok { 194 | return err 195 | } 196 | 197 | tp := retval.Type() 198 | 199 | // Support for time.Duration 200 | if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() { 201 | parsed, err := time.ParseDuration(val) 202 | 203 | if err != nil { 204 | return err 205 | } 206 | 207 | retval.SetInt(int64(parsed)) 208 | return nil 209 | } 210 | 211 | switch tp.Kind() { 212 | case reflect.String: 213 | retval.SetString(val) 214 | case reflect.Bool: 215 | if val == "" { 216 | retval.SetBool(true) 217 | } else { 218 | b, err := strconv.ParseBool(val) 219 | 220 | if err != nil { 221 | return err 222 | } 223 | 224 | retval.SetBool(b) 225 | } 226 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 227 | base, err := getBase(options, 0) 228 | 229 | if err != nil { 230 | return err 231 | } 232 | 233 | parsed, err := strconv.ParseInt(val, base, tp.Bits()) 234 | 235 | if err != nil { 236 | return err 237 | } 238 | 239 | retval.SetInt(parsed) 240 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 241 | base, err := getBase(options, 0) 242 | 243 | if err != nil { 244 | return err 245 | } 246 | 247 | parsed, err := strconv.ParseUint(val, base, tp.Bits()) 248 | 249 | if err != nil { 250 | return err 251 | } 252 | 253 | retval.SetUint(parsed) 254 | case reflect.Float32, reflect.Float64: 255 | parsed, err := strconv.ParseFloat(val, tp.Bits()) 256 | 257 | if err != nil { 258 | return err 259 | } 260 | 261 | retval.SetFloat(parsed) 262 | case reflect.Slice: 263 | elemtp := tp.Elem() 264 | 265 | elemvalptr := reflect.New(elemtp) 266 | elemval := reflect.Indirect(elemvalptr) 267 | 268 | if err := convert(val, elemval, options); err != nil { 269 | return err 270 | } 271 | 272 | retval.Set(reflect.Append(retval, elemval)) 273 | case reflect.Map: 274 | keyValueDelimiter := options.Get("key-value-delimiter") 275 | if keyValueDelimiter == "" { 276 | keyValueDelimiter = ":" 277 | } 278 | 279 | parts := strings.SplitN(val, keyValueDelimiter, 2) 280 | 281 | key := parts[0] 282 | var value string 283 | 284 | if len(parts) == 2 { 285 | value = parts[1] 286 | } 287 | 288 | keytp := tp.Key() 289 | keyval := reflect.New(keytp) 290 | 291 | if err := convert(key, keyval, options); err != nil { 292 | return err 293 | } 294 | 295 | valuetp := tp.Elem() 296 | valueval := reflect.New(valuetp) 297 | 298 | if err := convert(value, valueval, options); err != nil { 299 | return err 300 | } 301 | 302 | if retval.IsNil() { 303 | retval.Set(reflect.MakeMap(tp)) 304 | } 305 | 306 | retval.SetMapIndex(reflect.Indirect(keyval), reflect.Indirect(valueval)) 307 | case reflect.Ptr: 308 | if retval.IsNil() { 309 | retval.Set(reflect.New(retval.Type().Elem())) 310 | } 311 | 312 | return convert(val, reflect.Indirect(retval), options) 313 | case reflect.Interface: 314 | if !retval.IsNil() { 315 | return convert(val, retval.Elem(), options) 316 | } 317 | } 318 | 319 | return nil 320 | } 321 | 322 | func isPrint(s string) bool { 323 | for _, c := range s { 324 | if !strconv.IsPrint(c) { 325 | return false 326 | } 327 | } 328 | 329 | return true 330 | } 331 | 332 | func quoteIfNeeded(s string) string { 333 | if !isPrint(s) { 334 | return strconv.Quote(s) 335 | } 336 | 337 | return s 338 | } 339 | 340 | func quoteIfNeededV(s []string) []string { 341 | ret := make([]string, len(s)) 342 | 343 | for i, v := range s { 344 | ret[i] = quoteIfNeeded(v) 345 | } 346 | 347 | return ret 348 | } 349 | 350 | func quoteV(s []string) []string { 351 | ret := make([]string, len(s)) 352 | 353 | for i, v := range s { 354 | ret[i] = strconv.Quote(v) 355 | } 356 | 357 | return ret 358 | } 359 | 360 | func unquoteIfPossible(s string) (string, error) { 361 | if len(s) == 0 || s[0] != '"' { 362 | return s, nil 363 | } 364 | 365 | return strconv.Unquote(s) 366 | } 367 | -------------------------------------------------------------------------------- /convert_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func expectConvert(t *testing.T, o *Option, expected string) { 9 | s, err := convertToString(o.value, o.tag) 10 | 11 | if err != nil { 12 | t.Errorf("Unexpected error: %v", err) 13 | return 14 | } 15 | 16 | assertString(t, s, expected) 17 | } 18 | 19 | func TestConvertToString(t *testing.T) { 20 | d, _ := time.ParseDuration("1h2m4s") 21 | 22 | var opts = struct { 23 | String string `long:"string"` 24 | 25 | Int int `long:"int"` 26 | Int8 int8 `long:"int8"` 27 | Int16 int16 `long:"int16"` 28 | Int32 int32 `long:"int32"` 29 | Int64 int64 `long:"int64"` 30 | 31 | Uint uint `long:"uint"` 32 | Uint8 uint8 `long:"uint8"` 33 | Uint16 uint16 `long:"uint16"` 34 | Uint32 uint32 `long:"uint32"` 35 | Uint64 uint64 `long:"uint64"` 36 | 37 | Float32 float32 `long:"float32"` 38 | Float64 float64 `long:"float64"` 39 | 40 | Duration time.Duration `long:"duration"` 41 | 42 | Bool bool `long:"bool"` 43 | 44 | IntSlice []int `long:"int-slice"` 45 | IntFloatMap map[int]float64 `long:"int-float-map"` 46 | 47 | PtrBool *bool `long:"ptr-bool"` 48 | Interface interface{} `long:"interface"` 49 | 50 | Int32Base int32 `long:"int32-base" base:"16"` 51 | Uint32Base uint32 `long:"uint32-base" base:"16"` 52 | }{ 53 | "string", 54 | 55 | -2, 56 | -1, 57 | 0, 58 | 1, 59 | 2, 60 | 61 | 1, 62 | 2, 63 | 3, 64 | 4, 65 | 5, 66 | 67 | 1.2, 68 | -3.4, 69 | 70 | d, 71 | true, 72 | 73 | []int{-3, 4, -2}, 74 | map[int]float64{-2: 4.5}, 75 | 76 | new(bool), 77 | float32(5.2), 78 | 79 | -5823, 80 | 4232, 81 | } 82 | 83 | p := NewNamedParser("test", Default) 84 | grp, _ := p.AddGroup("test group", "", &opts) 85 | 86 | expects := []string{ 87 | "string", 88 | "-2", 89 | "-1", 90 | "0", 91 | "1", 92 | "2", 93 | 94 | "1", 95 | "2", 96 | "3", 97 | "4", 98 | "5", 99 | 100 | "1.2", 101 | "-3.4", 102 | 103 | "1h2m4s", 104 | "true", 105 | 106 | "[-3, 4, -2]", 107 | "{-2:4.5}", 108 | 109 | "false", 110 | "5.2", 111 | 112 | "-16bf", 113 | "1088", 114 | } 115 | 116 | for i, v := range grp.Options() { 117 | expectConvert(t, v, expects[i]) 118 | } 119 | } 120 | 121 | func TestConvertToStringInvalidIntBase(t *testing.T) { 122 | var opts = struct { 123 | Int int `long:"int" base:"no"` 124 | }{ 125 | 2, 126 | } 127 | 128 | p := NewNamedParser("test", Default) 129 | grp, _ := p.AddGroup("test group", "", &opts) 130 | o := grp.Options()[0] 131 | 132 | _, err := convertToString(o.value, o.tag) 133 | 134 | if err != nil { 135 | err = newErrorf(ErrMarshal, "%v", err) 136 | } 137 | 138 | assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax") 139 | } 140 | 141 | func TestConvertToStringInvalidUintBase(t *testing.T) { 142 | var opts = struct { 143 | Uint uint `long:"uint" base:"no"` 144 | }{ 145 | 2, 146 | } 147 | 148 | p := NewNamedParser("test", Default) 149 | grp, _ := p.AddGroup("test group", "", &opts) 150 | o := grp.Options()[0] 151 | 152 | _, err := convertToString(o.value, o.tag) 153 | 154 | if err != nil { 155 | err = newErrorf(ErrMarshal, "%v", err) 156 | } 157 | 158 | assertError(t, err, ErrMarshal, "strconv.ParseInt: parsing \"no\": invalid syntax") 159 | } 160 | 161 | func TestConvertToMapWithDelimiter(t *testing.T) { 162 | var opts = struct { 163 | StringStringMap map[string]string `long:"string-string-map" key-value-delimiter:"="` 164 | }{} 165 | 166 | p := NewNamedParser("test", Default) 167 | grp, _ := p.AddGroup("test group", "", &opts) 168 | o := grp.Options()[0] 169 | 170 | err := convert("key=value", o.value, o.tag) 171 | 172 | if err != nil { 173 | t.Errorf("Unexpected error: %v", err) 174 | return 175 | } 176 | 177 | assertString(t, opts.StringStringMap["key"], "value") 178 | } 179 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ErrorType represents the type of error. 8 | type ErrorType uint 9 | 10 | const ( 11 | // ErrUnknown indicates a generic error. 12 | ErrUnknown ErrorType = iota 13 | 14 | // ErrExpectedArgument indicates that an argument was expected. 15 | ErrExpectedArgument 16 | 17 | // ErrUnknownFlag indicates an unknown flag. 18 | ErrUnknownFlag 19 | 20 | // ErrUnknownGroup indicates an unknown group. 21 | ErrUnknownGroup 22 | 23 | // ErrMarshal indicates a marshalling error while converting values. 24 | ErrMarshal 25 | 26 | // ErrHelp indicates that the built-in help was shown (the error 27 | // contains the help message). 28 | ErrHelp 29 | 30 | // ErrNoArgumentForBool indicates that an argument was given for a 31 | // boolean flag (which don't not take any arguments). 32 | ErrNoArgumentForBool 33 | 34 | // ErrRequired indicates that a required flag was not provided. 35 | ErrRequired 36 | 37 | // ErrShortNameTooLong indicates that a short flag name was specified, 38 | // longer than one character. 39 | ErrShortNameTooLong 40 | 41 | // ErrDuplicatedFlag indicates that a short or long flag has been 42 | // defined more than once 43 | ErrDuplicatedFlag 44 | 45 | // ErrTag indicates an error while parsing flag tags. 46 | ErrTag 47 | 48 | // ErrCommandRequired indicates that a command was required but not 49 | // specified 50 | ErrCommandRequired 51 | 52 | // ErrUnknownCommand indicates that an unknown command was specified. 53 | ErrUnknownCommand 54 | 55 | // ErrInvalidChoice indicates an invalid option value which only allows 56 | // a certain number of choices. 57 | ErrInvalidChoice 58 | 59 | // ErrInvalidTag indicates an invalid tag or invalid use of an existing tag 60 | ErrInvalidTag 61 | ) 62 | 63 | func (e ErrorType) String() string { 64 | switch e { 65 | case ErrUnknown: 66 | return "unknown" 67 | case ErrExpectedArgument: 68 | return "expected argument" 69 | case ErrUnknownFlag: 70 | return "unknown flag" 71 | case ErrUnknownGroup: 72 | return "unknown group" 73 | case ErrMarshal: 74 | return "marshal" 75 | case ErrHelp: 76 | return "help" 77 | case ErrNoArgumentForBool: 78 | return "no argument for bool" 79 | case ErrRequired: 80 | return "required" 81 | case ErrShortNameTooLong: 82 | return "short name too long" 83 | case ErrDuplicatedFlag: 84 | return "duplicated flag" 85 | case ErrTag: 86 | return "tag" 87 | case ErrCommandRequired: 88 | return "command required" 89 | case ErrUnknownCommand: 90 | return "unknown command" 91 | case ErrInvalidChoice: 92 | return "invalid choice" 93 | case ErrInvalidTag: 94 | return "invalid tag" 95 | } 96 | 97 | return "unrecognized error type" 98 | } 99 | 100 | func (e ErrorType) Error() string { 101 | return e.String() 102 | } 103 | 104 | // Error represents a parser error. The error returned from Parse is of this 105 | // type. The error contains both a Type and Message. 106 | type Error struct { 107 | // The type of error 108 | Type ErrorType 109 | 110 | // The error message 111 | Message string 112 | } 113 | 114 | // Error returns the error's message 115 | func (e *Error) Error() string { 116 | return e.Message 117 | } 118 | 119 | func newError(tp ErrorType, message string) *Error { 120 | return &Error{ 121 | Type: tp, 122 | Message: message, 123 | } 124 | } 125 | 126 | func newErrorf(tp ErrorType, format string, args ...interface{}) *Error { 127 | return newError(tp, fmt.Sprintf(format, args...)) 128 | } 129 | 130 | func wrapError(err error) *Error { 131 | ret, ok := err.(*Error) 132 | 133 | if !ok { 134 | return newError(ErrUnknown, err.Error()) 135 | } 136 | 137 | return ret 138 | } 139 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Example of use of the flags package. 2 | package flags 3 | 4 | import ( 5 | "fmt" 6 | "os/exec" 7 | ) 8 | 9 | func Example() { 10 | var opts struct { 11 | // Slice of bool will append 'true' each time the option 12 | // is encountered (can be set multiple times, like -vvv) 13 | Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` 14 | 15 | // Example of automatic marshalling to desired type (uint) 16 | Offset uint `long:"offset" description:"Offset"` 17 | 18 | // Example of a callback, called each time the option is found. 19 | Call func(string) `short:"c" description:"Call phone number"` 20 | 21 | // Example of a required flag 22 | Name string `short:"n" long:"name" description:"A name" required:"true"` 23 | 24 | // Example of a value name 25 | File string `short:"f" long:"file" description:"A file" value-name:"FILE"` 26 | 27 | // Example of a pointer 28 | Ptr *int `short:"p" description:"A pointer to an integer"` 29 | 30 | // Example of a slice of strings 31 | StringSlice []string `short:"s" description:"A slice of strings"` 32 | 33 | // Example of a slice of pointers 34 | PtrSlice []*string `long:"ptrslice" description:"A slice of pointers to string"` 35 | 36 | // Example of a map 37 | IntMap map[string]int `long:"intmap" description:"A map from string to int"` 38 | 39 | // Example of a filename (useful for completion) 40 | Filename Filename `long:"filename" description:"A filename"` 41 | 42 | // Example of positional arguments 43 | Args struct { 44 | ID string 45 | Num int 46 | Rest []string 47 | } `positional-args:"yes" required:"yes"` 48 | } 49 | 50 | // Callback which will invoke callto: to call a number. 51 | // Note that this works just on OS X (and probably only with 52 | // Skype) but it shows the idea. 53 | opts.Call = func(num string) { 54 | cmd := exec.Command("open", "callto:"+num) 55 | cmd.Start() 56 | cmd.Process.Release() 57 | } 58 | 59 | // Make some fake arguments to parse. 60 | args := []string{ 61 | "-vv", 62 | "--offset=5", 63 | "-n", "Me", 64 | "-p", "3", 65 | "-s", "hello", 66 | "-s", "world", 67 | "--ptrslice", "hello", 68 | "--ptrslice", "world", 69 | "--intmap", "a:1", 70 | "--intmap", "b:5", 71 | "--filename", "hello.go", 72 | "id", 73 | "10", 74 | "remaining1", 75 | "remaining2", 76 | } 77 | 78 | // Parse flags from `args'. Note that here we use flags.ParseArgs for 79 | // the sake of making a working example. Normally, you would simply use 80 | // flags.Parse(&opts) which uses os.Args 81 | _, err := ParseArgs(&opts, args) 82 | 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | fmt.Printf("Verbosity: %v\n", opts.Verbose) 88 | fmt.Printf("Offset: %d\n", opts.Offset) 89 | fmt.Printf("Name: %s\n", opts.Name) 90 | fmt.Printf("Ptr: %d\n", *opts.Ptr) 91 | fmt.Printf("StringSlice: %v\n", opts.StringSlice) 92 | fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1]) 93 | fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"]) 94 | fmt.Printf("Filename: %v\n", opts.Filename) 95 | fmt.Printf("Args.ID: %s\n", opts.Args.ID) 96 | fmt.Printf("Args.Num: %d\n", opts.Args.Num) 97 | fmt.Printf("Args.Rest: %v\n", opts.Args.Rest) 98 | 99 | // Output: Verbosity: [true true] 100 | // Offset: 5 101 | // Name: Me 102 | // Ptr: 3 103 | // StringSlice: [hello world] 104 | // PtrSlice: [hello world] 105 | // IntMap: [a:1 b:5] 106 | // Filename: hello.go 107 | // Args.ID: id 108 | // Args.Num: 10 109 | // Args.Rest: [remaining1 remaining2] 110 | } 111 | -------------------------------------------------------------------------------- /examples/add.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type AddCommand struct { 8 | All bool `short:"a" long:"all" description:"Add all files"` 9 | } 10 | 11 | var addCommand AddCommand 12 | 13 | func (x *AddCommand) Execute(args []string) error { 14 | fmt.Printf("Adding (all=%v): %#v\n", x.All, args) 15 | return nil 16 | } 17 | 18 | func init() { 19 | parser.AddCommand("add", 20 | "Add a file", 21 | "The add command adds a file to the repository. Use -a to add all files.", 22 | &addCommand) 23 | } 24 | -------------------------------------------------------------------------------- /examples/bash-completion: -------------------------------------------------------------------------------- 1 | _examples() { 2 | args=("${COMP_WORDS[@]:1:$COMP_CWORD}") 3 | 4 | local IFS=$'\n' 5 | COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) 6 | return 1 7 | } 8 | 9 | complete -F _examples examples 10 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/jessevdk/go-flags" 7 | "os" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | type EditorOptions struct { 13 | Input flags.Filename `short:"i" long:"input" description:"Input file" default:"-"` 14 | Output flags.Filename `short:"o" long:"output" description:"Output file" default:"-"` 15 | } 16 | 17 | type Point struct { 18 | X, Y int 19 | } 20 | 21 | func (p *Point) UnmarshalFlag(value string) error { 22 | parts := strings.Split(value, ",") 23 | 24 | if len(parts) != 2 { 25 | return errors.New("expected two numbers separated by a ,") 26 | } 27 | 28 | x, err := strconv.ParseInt(parts[0], 10, 32) 29 | 30 | if err != nil { 31 | return err 32 | } 33 | 34 | y, err := strconv.ParseInt(parts[1], 10, 32) 35 | 36 | if err != nil { 37 | return err 38 | } 39 | 40 | p.X = int(x) 41 | p.Y = int(y) 42 | 43 | return nil 44 | } 45 | 46 | func (p Point) MarshalFlag() (string, error) { 47 | return fmt.Sprintf("%d,%d", p.X, p.Y), nil 48 | } 49 | 50 | type Options struct { 51 | // Example of verbosity with level 52 | Verbose []bool `short:"v" long:"verbose" description:"Verbose output"` 53 | 54 | // Example of optional value 55 | User string `short:"u" long:"user" description:"User name" optional:"yes" optional-value:"pancake"` 56 | 57 | // Example of map with multiple default values 58 | Users map[string]string `long:"users" description:"User e-mail map" default:"system:system@example.org" default:"admin:admin@example.org"` 59 | 60 | // Example of option group 61 | Editor EditorOptions `group:"Editor Options"` 62 | 63 | // Example of custom type Marshal/Unmarshal 64 | Point Point `long:"point" description:"A x,y point" default:"1,2"` 65 | } 66 | 67 | var options Options 68 | 69 | var parser = flags.NewParser(&options, flags.Default) 70 | 71 | func main() { 72 | if _, err := parser.Parse(); err != nil { 73 | switch flagsErr := err.(type) { 74 | case flags.ErrorType: 75 | if flagsErr == flags.ErrHelp { 76 | os.Exit(0) 77 | } 78 | os.Exit(1) 79 | default: 80 | os.Exit(1) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/rm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type RmCommand struct { 8 | Force bool `short:"f" long:"force" description:"Force removal of files"` 9 | } 10 | 11 | var rmCommand RmCommand 12 | 13 | func (x *RmCommand) Execute(args []string) error { 14 | fmt.Printf("Removing (force=%v): %#v\n", x.Force, args) 15 | return nil 16 | } 17 | 18 | func init() { 19 | parser.AddCommand("rm", 20 | "Remove a file", 21 | "The rm command removes a file to the repository. Use -f to force removal of files.", 22 | &rmCommand) 23 | } 24 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Jesse van den Kieboom. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package flags provides an extensive command line option parser. 7 | The flags package is similar in functionality to the go built-in flag package 8 | but provides more options and uses reflection to provide a convenient and 9 | succinct way of specifying command line options. 10 | 11 | # Supported features 12 | 13 | The following features are supported in go-flags: 14 | 15 | Options with short names (-v) 16 | Options with long names (--verbose) 17 | Options with and without arguments (bool v.s. other type) 18 | Options with optional arguments and default values 19 | Option default values from ENVIRONMENT_VARIABLES, including slice and map values 20 | Multiple option groups each containing a set of options 21 | Generate and print well-formatted help message 22 | Passing remaining command line arguments after -- (optional) 23 | Ignoring unknown command line options (optional) 24 | Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification 25 | Supports multiple short options -aux 26 | Supports all primitive go types (string, int{8..64}, uint{8..64}, float) 27 | Supports same option multiple times (can store in slice or last option counts) 28 | Supports maps 29 | Supports function callbacks 30 | Supports namespaces for (nested) option groups 31 | 32 | Additional features specific to Windows: 33 | 34 | Options with short names (/v) 35 | Options with long names (/verbose) 36 | Windows-style options with arguments use a colon as the delimiter 37 | Modify generated help message with Windows-style / options 38 | Windows style options can be disabled at build time using the "forceposix" 39 | build tag 40 | 41 | # Basic usage 42 | 43 | The flags package uses structs, reflection and struct field tags 44 | to allow users to specify command line options. This results in very simple 45 | and concise specification of your application options. For example: 46 | 47 | type Options struct { 48 | Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` 49 | } 50 | 51 | This specifies one option with a short name -v and a long name --verbose. 52 | When either -v or --verbose is found on the command line, a 'true' value 53 | will be appended to the Verbose field. e.g. when specifying -vvv, the 54 | resulting value of Verbose will be {[true, true, true]}. 55 | 56 | Slice options work exactly the same as primitive type options, except that 57 | whenever the option is encountered, a value is appended to the slice. 58 | 59 | Map options from string to primitive type are also supported. On the command 60 | line, you specify the value for such an option as key:value. For example 61 | 62 | type Options struct { 63 | AuthorInfo string[string] `short:"a"` 64 | } 65 | 66 | Then, the AuthorInfo map can be filled with something like 67 | -a name:Jesse -a "surname:van den Kieboom". 68 | 69 | Finally, for full control over the conversion between command line argument 70 | values and options, user defined types can choose to implement the Marshaler 71 | and Unmarshaler interfaces. 72 | 73 | # Available field tags 74 | 75 | The following is a list of tags for struct fields supported by go-flags: 76 | 77 | short: the short name of the option (single character) 78 | long: the long name of the option 79 | required: if non empty, makes the option required to appear on the command 80 | line. If a required option is not present, the parser will 81 | return ErrRequired (optional) 82 | description: the description of the option (optional) 83 | long-description: the long description of the option. Currently only 84 | displayed in generated man pages (optional) 85 | no-flag: if non-empty, this field is ignored as an option (optional) 86 | 87 | optional: if non-empty, makes the argument of the option optional. When an 88 | argument is optional it can only be specified using 89 | --option=argument (optional) 90 | optional-value: the value of an optional option when the option occurs 91 | without an argument. This tag can be specified multiple 92 | times in the case of maps or slices (optional) 93 | default: the default value of an option. This tag can be specified 94 | multiple times in the case of slices or maps (optional) 95 | default-mask: when specified, this value will be displayed in the help 96 | instead of the actual default value. This is useful 97 | mostly for hiding otherwise sensitive information from 98 | showing up in the help. If default-mask takes the special 99 | value "-", then no default value will be shown at all 100 | (optional) 101 | env: the default value of the option is overridden from the 102 | specified environment variable, if one has been defined. 103 | (optional) 104 | env-delim: the 'env' default value from environment is split into 105 | multiple values with the given delimiter string, use with 106 | slices and maps (optional) 107 | value-name: the name of the argument value (to be shown in the help) 108 | (optional) 109 | choice: limits the values for an option to a set of values. 110 | Repeat this tag once for each allowable value. 111 | e.g. `long:"animal" choice:"cat" choice:"dog"` 112 | hidden: if non-empty, the option is not visible in the help or man page. 113 | 114 | base: a base (radix) used to convert strings to integer values, the 115 | default base is 10 (i.e. decimal) (optional) 116 | 117 | ini-name: the explicit ini option name (optional) 118 | no-ini: if non-empty this field is ignored as an ini option 119 | (optional) 120 | 121 | group: when specified on a struct field, makes the struct 122 | field a separate group with the given name (optional) 123 | namespace: when specified on a group struct field, the namespace 124 | gets prepended to every option's long name and 125 | subgroup's namespace of this group, separated by 126 | the parser's namespace delimiter (optional) 127 | env-namespace: when specified on a group struct field, the env-namespace 128 | gets prepended to every option's env key and 129 | subgroup's env-namespace of this group, separated by 130 | the parser's env-namespace delimiter (optional) 131 | command: when specified on a struct field, makes the struct 132 | field a (sub)command with the given name (optional) 133 | subcommands-optional: when specified on a command struct field, makes 134 | any subcommands of that command optional (optional) 135 | alias: when specified on a command struct field, adds the 136 | specified name as an alias for the command. Can be 137 | be specified multiple times to add more than one 138 | alias (optional) 139 | positional-args: when specified on a field with a struct type, 140 | uses the fields of that struct to parse remaining 141 | positional command line arguments into (in order 142 | of the fields). If a field has a slice type, 143 | then all remaining arguments will be added to it. 144 | Positional arguments are optional by default, 145 | unless the "required" tag is specified together 146 | with the "positional-args" tag. The "required" tag 147 | can also be set on the individual rest argument 148 | fields, to require only the first N positional 149 | arguments. If the "required" tag is set on the 150 | rest arguments slice, then its value determines 151 | the minimum amount of rest arguments that needs to 152 | be provided (e.g. `required:"2"`) (optional) 153 | positional-arg-name: used on a field in a positional argument struct; name 154 | of the positional argument placeholder to be shown in 155 | the help (optional) 156 | 157 | Either the `short:` tag or the `long:` must be specified to make the field eligible as an 158 | option. 159 | 160 | # Option groups 161 | 162 | Option groups are a simple way to semantically separate your options. All 163 | options in a particular group are shown together in the help under the name 164 | of the group. Namespaces can be used to specify option long names more 165 | precisely and emphasize the options affiliation to their group. 166 | 167 | There are currently three ways to specify option groups. 168 | 169 | 1. Use NewNamedParser specifying the various option groups. 170 | 2. Use AddGroup to add a group to an existing parser. 171 | 3. Add a struct field to the top-level options annotated with the 172 | group:"group-name" tag. 173 | 174 | # Commands 175 | 176 | The flags package also has basic support for commands. Commands are often 177 | used in monolithic applications that support various commands or actions. 178 | Take git for example, all of the add, commit, checkout, etc. are called 179 | commands. Using commands you can easily separate multiple functions of your 180 | application. 181 | 182 | There are currently two ways to specify a command. 183 | 184 | 1. Use AddCommand on an existing parser. 185 | 2. Add a struct field to your options struct annotated with the 186 | command:"command-name" tag. 187 | 188 | The most common, idiomatic way to implement commands is to define a global 189 | parser instance and implement each command in a separate file. These 190 | command files should define a go init function which calls AddCommand on 191 | the global parser. 192 | 193 | When parsing ends and there is an active command and that command implements 194 | the Commander interface, then its Execute method will be run with the 195 | remaining command line arguments. 196 | 197 | Command structs can have options which become valid to parse after the 198 | command has been specified on the command line, in addition to the options 199 | of all the parent commands. I.e. considering a -v flag on the parser and an 200 | add command, the following are equivalent: 201 | 202 | ./app -v add 203 | ./app add -v 204 | 205 | However, if the -v flag is defined on the add command, then the first of 206 | the two examples above would fail since the -v flag is not defined before 207 | the add command. 208 | 209 | # Completion 210 | 211 | go-flags has builtin support to provide bash completion of flags, commands 212 | and argument values. To use completion, the binary which uses go-flags 213 | can be invoked in a special environment to list completion of the current 214 | command line argument. It should be noted that this `executes` your application, 215 | and it is up to the user to make sure there are no negative side effects (for 216 | example from init functions). 217 | 218 | Setting the environment variable `GO_FLAGS_COMPLETION=1` enables completion 219 | by replacing the argument parsing routine with the completion routine which 220 | outputs completions for the passed arguments. The basic invocation to 221 | complete a set of arguments is therefore: 222 | 223 | GO_FLAGS_COMPLETION=1 ./completion-example arg1 arg2 arg3 224 | 225 | where `completion-example` is the binary, `arg1` and `arg2` are 226 | the current arguments, and `arg3` (the last argument) is the argument 227 | to be completed. If the GO_FLAGS_COMPLETION is set to "verbose", then 228 | descriptions of possible completion items will also be shown, if there 229 | are more than 1 completion items. 230 | 231 | To use this with bash completion, a simple file can be written which 232 | calls the binary which supports go-flags completion: 233 | 234 | _completion_example() { 235 | # All arguments except the first one 236 | args=("${COMP_WORDS[@]:1:$COMP_CWORD}") 237 | 238 | # Only split on newlines 239 | local IFS=$'\n' 240 | 241 | # Call completion (note that the first element of COMP_WORDS is 242 | # the executable itself) 243 | COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) 244 | return 0 245 | } 246 | 247 | complete -F _completion_example completion-example 248 | 249 | Completion requires the parser option PassDoubleDash and is therefore enforced if the environment variable GO_FLAGS_COMPLETION is set. 250 | 251 | Customized completion for argument values is supported by implementing 252 | the flags.Completer interface for the argument value type. An example 253 | of a type which does so is the flags.Filename type, an alias of string 254 | allowing simple filename completion. A slice or array argument value 255 | whose element type implements flags.Completer will also be completed. 256 | */ 257 | package flags 258 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jessevdk/go-flags 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/sergi/go-diff v1.3.1 7 | golang.org/x/sys v0.21.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 10 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 13 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 14 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 15 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 19 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 20 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 21 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Jesse van den Kieboom. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package flags 6 | 7 | import ( 8 | "errors" 9 | "reflect" 10 | "strings" 11 | "unicode/utf8" 12 | ) 13 | 14 | // ErrNotPointerToStruct indicates that a provided data container is not 15 | // a pointer to a struct. Only pointers to structs are valid data containers 16 | // for options. 17 | var ErrNotPointerToStruct = errors.New("provided data is not a pointer to struct") 18 | 19 | // Group represents an option group. Option groups can be used to logically 20 | // group options together under a description. Groups are only used to provide 21 | // more structure to options both for the user (as displayed in the help message) 22 | // and for you, since groups can be nested. 23 | type Group struct { 24 | // A short description of the group. The 25 | // short description is primarily used in the built-in generated help 26 | // message 27 | ShortDescription string 28 | 29 | // A long description of the group. The long 30 | // description is primarily used to present information on commands 31 | // (Command embeds Group) in the built-in generated help and man pages. 32 | LongDescription string 33 | 34 | // The namespace of the group 35 | Namespace string 36 | 37 | // The environment namespace of the group 38 | EnvNamespace string 39 | 40 | // If true, the group is not displayed in the help or man page 41 | Hidden bool 42 | 43 | // The parent of the group or nil if it has no parent 44 | parent interface{} 45 | 46 | // All the options in the group 47 | options []*Option 48 | 49 | // All the subgroups 50 | groups []*Group 51 | 52 | // Whether the group represents the built-in help group 53 | isBuiltinHelp bool 54 | 55 | data interface{} 56 | } 57 | 58 | type scanHandler func(reflect.Value, *reflect.StructField) (bool, error) 59 | 60 | // AddGroup adds a new group to the command with the given name and data. The 61 | // data needs to be a pointer to a struct from which the fields indicate which 62 | // options are in the group. 63 | func (g *Group) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) { 64 | group := newGroup(shortDescription, longDescription, data) 65 | 66 | group.parent = g 67 | 68 | if err := group.scan(); err != nil { 69 | return nil, err 70 | } 71 | 72 | g.groups = append(g.groups, group) 73 | return group, nil 74 | } 75 | 76 | // AddOption adds a new option to this group. 77 | func (g *Group) AddOption(option *Option, data interface{}) { 78 | option.value = reflect.ValueOf(data) 79 | option.group = g 80 | g.options = append(g.options, option) 81 | } 82 | 83 | // Groups returns the list of groups embedded in this group. 84 | func (g *Group) Groups() []*Group { 85 | return g.groups 86 | } 87 | 88 | // Options returns the list of options in this group. 89 | func (g *Group) Options() []*Option { 90 | return g.options 91 | } 92 | 93 | // Find locates the subgroup with the given short description and returns it. 94 | // If no such group can be found Find will return nil. Note that the description 95 | // is matched case insensitively. 96 | func (g *Group) Find(shortDescription string) *Group { 97 | lshortDescription := strings.ToLower(shortDescription) 98 | 99 | var ret *Group 100 | 101 | g.eachGroup(func(gg *Group) { 102 | if gg != g && strings.ToLower(gg.ShortDescription) == lshortDescription { 103 | ret = gg 104 | } 105 | }) 106 | 107 | return ret 108 | } 109 | 110 | func (g *Group) findOption(matcher func(*Option) bool) (option *Option) { 111 | g.eachGroup(func(g *Group) { 112 | for _, opt := range g.options { 113 | if option == nil && matcher(opt) { 114 | option = opt 115 | } 116 | } 117 | }) 118 | 119 | return option 120 | } 121 | 122 | // FindOptionByLongName finds an option that is part of the group, or any of its 123 | // subgroups, by matching its long name (including the option namespace). 124 | func (g *Group) FindOptionByLongName(longName string) *Option { 125 | return g.findOption(func(option *Option) bool { 126 | return option.LongNameWithNamespace() == longName 127 | }) 128 | } 129 | 130 | // FindOptionByShortName finds an option that is part of the group, or any of 131 | // its subgroups, by matching its short name. 132 | func (g *Group) FindOptionByShortName(shortName rune) *Option { 133 | return g.findOption(func(option *Option) bool { 134 | return option.ShortName == shortName 135 | }) 136 | } 137 | 138 | func newGroup(shortDescription string, longDescription string, data interface{}) *Group { 139 | return &Group{ 140 | ShortDescription: shortDescription, 141 | LongDescription: longDescription, 142 | 143 | data: data, 144 | } 145 | } 146 | 147 | func (g *Group) optionByName(name string, namematch func(*Option, string) bool) *Option { 148 | prio := 0 149 | var retopt *Option 150 | 151 | g.eachGroup(func(g *Group) { 152 | for _, opt := range g.options { 153 | if namematch != nil && namematch(opt, name) && prio < 4 { 154 | retopt = opt 155 | prio = 4 156 | } 157 | 158 | if name == opt.field.Name && prio < 3 { 159 | retopt = opt 160 | prio = 3 161 | } 162 | 163 | if name == opt.LongNameWithNamespace() && prio < 2 { 164 | retopt = opt 165 | prio = 2 166 | } 167 | 168 | if opt.ShortName != 0 && name == string(opt.ShortName) && prio < 1 { 169 | retopt = opt 170 | prio = 1 171 | } 172 | } 173 | }) 174 | 175 | return retopt 176 | } 177 | 178 | func (g *Group) showInHelp() bool { 179 | if g.Hidden { 180 | return false 181 | } 182 | for _, opt := range g.options { 183 | if opt.showInHelp() { 184 | return true 185 | } 186 | } 187 | return false 188 | } 189 | 190 | func (g *Group) eachGroup(f func(*Group)) { 191 | f(g) 192 | 193 | for _, gg := range g.groups { 194 | gg.eachGroup(f) 195 | } 196 | } 197 | 198 | func isStringFalsy(s string) bool { 199 | return s == "" || s == "false" || s == "no" || s == "0" 200 | } 201 | 202 | func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, handler scanHandler) error { 203 | stype := realval.Type() 204 | 205 | if sfield != nil { 206 | if ok, err := handler(realval, sfield); err != nil { 207 | return err 208 | } else if ok { 209 | return nil 210 | } 211 | } 212 | 213 | for i := 0; i < stype.NumField(); i++ { 214 | field := stype.Field(i) 215 | 216 | // PkgName is set only for non-exported fields, which we ignore 217 | if field.PkgPath != "" && !field.Anonymous { 218 | continue 219 | } 220 | 221 | mtag := newMultiTag(string(field.Tag)) 222 | 223 | if err := mtag.Parse(); err != nil { 224 | return err 225 | } 226 | 227 | // Skip fields with the no-flag tag 228 | if mtag.Get("no-flag") != "" { 229 | continue 230 | } 231 | 232 | // Dive deep into structs or pointers to structs 233 | kind := field.Type.Kind() 234 | fld := realval.Field(i) 235 | 236 | if kind == reflect.Struct { 237 | if err := g.scanStruct(fld, &field, handler); err != nil { 238 | return err 239 | } 240 | } else if kind == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct { 241 | flagCountBefore := len(g.options) + len(g.groups) 242 | 243 | if fld.IsNil() { 244 | fld = reflect.New(fld.Type().Elem()) 245 | } 246 | 247 | if err := g.scanStruct(reflect.Indirect(fld), &field, handler); err != nil { 248 | return err 249 | } 250 | 251 | if len(g.options)+len(g.groups) != flagCountBefore { 252 | realval.Field(i).Set(fld) 253 | } 254 | } 255 | 256 | longname := mtag.Get("long") 257 | shortname := mtag.Get("short") 258 | 259 | // Need at least either a short or long name 260 | if longname == "" && shortname == "" && mtag.Get("ini-name") == "" { 261 | continue 262 | } 263 | 264 | short := rune(0) 265 | rc := utf8.RuneCountInString(shortname) 266 | 267 | if rc > 1 { 268 | return newErrorf(ErrShortNameTooLong, 269 | "short names can only be 1 character long, not `%s'", 270 | shortname) 271 | 272 | } else if rc == 1 { 273 | short, _ = utf8.DecodeRuneInString(shortname) 274 | } 275 | 276 | description := mtag.Get("description") 277 | def := mtag.GetMany("default") 278 | 279 | optionalValue := mtag.GetMany("optional-value") 280 | valueName := mtag.Get("value-name") 281 | defaultMask := mtag.Get("default-mask") 282 | 283 | optional := !isStringFalsy(mtag.Get("optional")) 284 | required := !isStringFalsy(mtag.Get("required")) 285 | choices := mtag.GetMany("choice") 286 | hidden := !isStringFalsy(mtag.Get("hidden")) 287 | 288 | option := &Option{ 289 | Description: description, 290 | ShortName: short, 291 | LongName: longname, 292 | Default: def, 293 | EnvDefaultKey: mtag.Get("env"), 294 | EnvDefaultDelim: mtag.Get("env-delim"), 295 | OptionalArgument: optional, 296 | OptionalValue: optionalValue, 297 | Required: required, 298 | ValueName: valueName, 299 | DefaultMask: defaultMask, 300 | Choices: choices, 301 | Hidden: hidden, 302 | 303 | group: g, 304 | 305 | field: field, 306 | value: realval.Field(i), 307 | tag: mtag, 308 | } 309 | 310 | if option.isBool() && option.Default != nil { 311 | return newErrorf(ErrInvalidTag, 312 | "boolean flag `%s' may not have default values, they always default to `false' and can only be turned on", 313 | option.shortAndLongName()) 314 | } 315 | 316 | g.options = append(g.options, option) 317 | } 318 | 319 | return nil 320 | } 321 | 322 | func (g *Group) checkForDuplicateFlags() *Error { 323 | shortNames := make(map[rune]*Option) 324 | longNames := make(map[string]*Option) 325 | 326 | var duplicateError *Error 327 | 328 | g.eachGroup(func(g *Group) { 329 | for _, option := range g.options { 330 | if option.LongName != "" { 331 | longName := option.LongNameWithNamespace() 332 | 333 | if otherOption, ok := longNames[longName]; ok { 334 | duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same long name as option `%s'", option, otherOption) 335 | return 336 | } 337 | longNames[longName] = option 338 | } 339 | if option.ShortName != 0 { 340 | if otherOption, ok := shortNames[option.ShortName]; ok { 341 | duplicateError = newErrorf(ErrDuplicatedFlag, "option `%s' uses the same short name as option `%s'", option, otherOption) 342 | return 343 | } 344 | shortNames[option.ShortName] = option 345 | } 346 | } 347 | }) 348 | 349 | return duplicateError 350 | } 351 | 352 | func (g *Group) scanSubGroupHandler(realval reflect.Value, sfield *reflect.StructField) (bool, error) { 353 | mtag := newMultiTag(string(sfield.Tag)) 354 | 355 | if err := mtag.Parse(); err != nil { 356 | return true, err 357 | } 358 | 359 | subgroup := mtag.Get("group") 360 | 361 | if len(subgroup) != 0 { 362 | var ptrval reflect.Value 363 | 364 | if realval.Kind() == reflect.Ptr { 365 | ptrval = realval 366 | 367 | if ptrval.IsNil() { 368 | ptrval.Set(reflect.New(ptrval.Type())) 369 | } 370 | } else { 371 | ptrval = realval.Addr() 372 | } 373 | 374 | description := mtag.Get("description") 375 | 376 | group, err := g.AddGroup(subgroup, description, ptrval.Interface()) 377 | 378 | if err != nil { 379 | return true, err 380 | } 381 | 382 | group.Namespace = mtag.Get("namespace") 383 | group.EnvNamespace = mtag.Get("env-namespace") 384 | group.Hidden = mtag.Get("hidden") != "" 385 | 386 | return true, nil 387 | } 388 | 389 | return false, nil 390 | } 391 | 392 | func (g *Group) scanType(handler scanHandler) error { 393 | // Get all the public fields in the data struct 394 | ptrval := reflect.ValueOf(g.data) 395 | 396 | if ptrval.Type().Kind() != reflect.Ptr { 397 | panic(ErrNotPointerToStruct) 398 | } 399 | 400 | stype := ptrval.Type().Elem() 401 | 402 | if stype.Kind() != reflect.Struct { 403 | panic(ErrNotPointerToStruct) 404 | } 405 | 406 | realval := reflect.Indirect(ptrval) 407 | 408 | if err := g.scanStruct(realval, nil, handler); err != nil { 409 | return err 410 | } 411 | 412 | if err := g.checkForDuplicateFlags(); err != nil { 413 | return err 414 | } 415 | 416 | return nil 417 | } 418 | 419 | func (g *Group) scan() error { 420 | return g.scanType(g.scanSubGroupHandler) 421 | } 422 | 423 | func (g *Group) groupByName(name string) *Group { 424 | if len(name) == 0 { 425 | return g 426 | } 427 | 428 | return g.Find(name) 429 | } 430 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGroupInline(t *testing.T) { 8 | var opts = struct { 9 | Value bool `short:"v"` 10 | 11 | Group struct { 12 | G bool `short:"g"` 13 | } `group:"Grouped Options"` 14 | }{} 15 | 16 | p, ret := assertParserSuccess(t, &opts, "-v", "-g") 17 | 18 | assertStringArray(t, ret, []string{}) 19 | 20 | if !opts.Value { 21 | t.Errorf("Expected Value to be true") 22 | } 23 | 24 | if !opts.Group.G { 25 | t.Errorf("Expected Group.G to be true") 26 | } 27 | 28 | if p.Command.Group.Find("Grouped Options") == nil { 29 | t.Errorf("Expected to find group `Grouped Options'") 30 | } 31 | } 32 | 33 | func TestGroupAdd(t *testing.T) { 34 | var opts = struct { 35 | Value bool `short:"v"` 36 | }{} 37 | 38 | var grp = struct { 39 | G bool `short:"g"` 40 | }{} 41 | 42 | p := NewParser(&opts, Default) 43 | g, err := p.AddGroup("Grouped Options", "", &grp) 44 | 45 | if err != nil { 46 | t.Fatalf("Unexpected error: %v", err) 47 | return 48 | } 49 | 50 | ret, err := p.ParseArgs([]string{"-v", "-g", "rest"}) 51 | 52 | if err != nil { 53 | t.Fatalf("Unexpected error: %v", err) 54 | return 55 | } 56 | 57 | assertStringArray(t, ret, []string{"rest"}) 58 | 59 | if !opts.Value { 60 | t.Errorf("Expected Value to be true") 61 | } 62 | 63 | if !grp.G { 64 | t.Errorf("Expected Group.G to be true") 65 | } 66 | 67 | if p.Command.Group.Find("Grouped Options") != g { 68 | t.Errorf("Expected to find group `Grouped Options'") 69 | } 70 | 71 | if p.Groups()[1] != g { 72 | t.Errorf("Expected group %#v, but got %#v", g, p.Groups()[0]) 73 | } 74 | 75 | if g.Options()[0].ShortName != 'g' { 76 | t.Errorf("Expected short name `g' but got %v", g.Options()[0].ShortName) 77 | } 78 | } 79 | 80 | func TestGroupNestedInline(t *testing.T) { 81 | var opts = struct { 82 | Value bool `short:"v"` 83 | 84 | Group struct { 85 | G bool `short:"g"` 86 | 87 | Nested struct { 88 | N string `long:"n"` 89 | } `group:"Nested Options"` 90 | } `group:"Grouped Options"` 91 | }{} 92 | 93 | p, ret := assertParserSuccess(t, &opts, "-v", "-g", "--n", "n", "rest") 94 | 95 | assertStringArray(t, ret, []string{"rest"}) 96 | 97 | if !opts.Value { 98 | t.Errorf("Expected Value to be true") 99 | } 100 | 101 | if !opts.Group.G { 102 | t.Errorf("Expected Group.G to be true") 103 | } 104 | 105 | assertString(t, opts.Group.Nested.N, "n") 106 | 107 | if p.Command.Group.Find("Grouped Options") == nil { 108 | t.Errorf("Expected to find group `Grouped Options'") 109 | } 110 | 111 | if p.Command.Group.Find("Nested Options") == nil { 112 | t.Errorf("Expected to find group `Nested Options'") 113 | } 114 | } 115 | 116 | func TestGroupNestedInlineNamespace(t *testing.T) { 117 | var opts = struct { 118 | Opt string `long:"opt"` 119 | 120 | Group struct { 121 | Opt string `long:"opt"` 122 | Group struct { 123 | Opt string `long:"opt"` 124 | } `group:"Subsubgroup" namespace:"sap"` 125 | } `group:"Subgroup" namespace:"sip"` 126 | }{} 127 | 128 | p, ret := assertParserSuccess(t, &opts, "--opt", "a", "--sip.opt", "b", "--sip.sap.opt", "c", "rest") 129 | 130 | assertStringArray(t, ret, []string{"rest"}) 131 | 132 | assertString(t, opts.Opt, "a") 133 | assertString(t, opts.Group.Opt, "b") 134 | assertString(t, opts.Group.Group.Opt, "c") 135 | 136 | for _, name := range []string{"Subgroup", "Subsubgroup"} { 137 | if p.Command.Group.Find(name) == nil { 138 | t.Errorf("Expected to find group '%s'", name) 139 | } 140 | } 141 | } 142 | 143 | func TestDuplicateShortFlags(t *testing.T) { 144 | var opts struct { 145 | Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` 146 | Variables []string `short:"v" long:"variable" description:"Set a variable value."` 147 | } 148 | 149 | args := []string{ 150 | "--verbose", 151 | "-v", "123", 152 | "-v", "456", 153 | } 154 | 155 | _, err := ParseArgs(&opts, args) 156 | 157 | if err == nil { 158 | t.Errorf("Expected an error with type ErrDuplicatedFlag") 159 | } else { 160 | err2 := err.(*Error) 161 | if err2.Type != ErrDuplicatedFlag { 162 | t.Errorf("Expected an error with type ErrDuplicatedFlag") 163 | } 164 | } 165 | } 166 | 167 | func TestDuplicateLongFlags(t *testing.T) { 168 | var opts struct { 169 | Test1 []bool `short:"a" long:"testing" description:"Test 1"` 170 | Test2 []string `short:"b" long:"testing" description:"Test 2."` 171 | } 172 | 173 | args := []string{ 174 | "--testing", 175 | } 176 | 177 | _, err := ParseArgs(&opts, args) 178 | 179 | if err == nil { 180 | t.Errorf("Expected an error with type ErrDuplicatedFlag") 181 | } else { 182 | err2 := err.(*Error) 183 | if err2.Type != ErrDuplicatedFlag { 184 | t.Errorf("Expected an error with type ErrDuplicatedFlag") 185 | } 186 | } 187 | } 188 | 189 | func TestFindOptionByLongFlag(t *testing.T) { 190 | var opts struct { 191 | Testing bool `long:"testing" description:"Testing"` 192 | } 193 | 194 | p := NewParser(&opts, Default) 195 | opt := p.FindOptionByLongName("testing") 196 | 197 | if opt == nil { 198 | t.Errorf("Expected option, but found none") 199 | } 200 | 201 | assertString(t, opt.LongName, "testing") 202 | } 203 | 204 | func TestFindOptionByShortFlag(t *testing.T) { 205 | var opts struct { 206 | Testing bool `short:"t" description:"Testing"` 207 | } 208 | 209 | p := NewParser(&opts, Default) 210 | opt := p.FindOptionByShortName('t') 211 | 212 | if opt == nil { 213 | t.Errorf("Expected option, but found none") 214 | } 215 | 216 | if opt.ShortName != 't' { 217 | t.Errorf("Expected 't', but got %v", opt.ShortName) 218 | } 219 | } 220 | 221 | func TestFindOptionByLongFlagInSubGroup(t *testing.T) { 222 | var opts struct { 223 | Group struct { 224 | Testing bool `long:"testing" description:"Testing"` 225 | } `group:"sub-group"` 226 | } 227 | 228 | p := NewParser(&opts, Default) 229 | opt := p.FindOptionByLongName("testing") 230 | 231 | if opt == nil { 232 | t.Errorf("Expected option, but found none") 233 | } 234 | 235 | assertString(t, opt.LongName, "testing") 236 | } 237 | 238 | func TestFindOptionByShortFlagInSubGroup(t *testing.T) { 239 | var opts struct { 240 | Group struct { 241 | Testing bool `short:"t" description:"Testing"` 242 | } `group:"sub-group"` 243 | } 244 | 245 | p := NewParser(&opts, Default) 246 | opt := p.FindOptionByShortName('t') 247 | 248 | if opt == nil { 249 | t.Errorf("Expected option, but found none") 250 | } 251 | 252 | if opt.ShortName != 't' { 253 | t.Errorf("Expected 't', but got %v", opt.ShortName) 254 | } 255 | } 256 | 257 | func TestAddOptionNonOptional(t *testing.T) { 258 | var opts struct { 259 | Test bool 260 | } 261 | p := NewParser(&opts, Default) 262 | p.AddOption(&Option{ 263 | LongName: "test", 264 | }, &opts.Test) 265 | _, err := p.ParseArgs([]string{"--test"}) 266 | if err != nil { 267 | t.Errorf("unexpected error: %s", err) 268 | } else if !opts.Test { 269 | t.Errorf("option not set") 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Jesse van den Kieboom. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package flags 6 | 7 | import ( 8 | "bufio" 9 | "bytes" 10 | "fmt" 11 | "io" 12 | "runtime" 13 | "strings" 14 | "unicode/utf8" 15 | ) 16 | 17 | type alignmentInfo struct { 18 | maxLongLen int 19 | hasShort bool 20 | hasValueName bool 21 | terminalColumns int 22 | indent bool 23 | } 24 | 25 | const ( 26 | paddingBeforeOption = 2 27 | distanceBetweenOptionAndDescription = 2 28 | ) 29 | 30 | func (a *alignmentInfo) descriptionStart() int { 31 | ret := a.maxLongLen + distanceBetweenOptionAndDescription 32 | 33 | if a.hasShort { 34 | ret += 2 35 | } 36 | 37 | if a.maxLongLen > 0 { 38 | ret += 4 39 | } 40 | 41 | if a.hasValueName { 42 | ret += 3 43 | } 44 | 45 | return ret 46 | } 47 | 48 | func (a *alignmentInfo) updateLen(name string, indent bool) { 49 | l := utf8.RuneCountInString(name) 50 | 51 | if indent { 52 | l = l + 4 53 | } 54 | 55 | if l > a.maxLongLen { 56 | a.maxLongLen = l 57 | } 58 | } 59 | 60 | func (p *Parser) getAlignmentInfo() alignmentInfo { 61 | ret := alignmentInfo{ 62 | maxLongLen: 0, 63 | hasShort: false, 64 | hasValueName: false, 65 | terminalColumns: getTerminalColumns(), 66 | } 67 | 68 | if ret.terminalColumns <= 0 { 69 | ret.terminalColumns = 80 70 | } 71 | 72 | var prevcmd *Command 73 | 74 | p.eachActiveGroup(func(c *Command, grp *Group) { 75 | if c != prevcmd { 76 | for _, arg := range c.args { 77 | ret.updateLen(arg.Name, c != p.Command) 78 | } 79 | prevcmd = c 80 | } 81 | if !grp.showInHelp() { 82 | return 83 | } 84 | for _, info := range grp.options { 85 | if !info.showInHelp() { 86 | continue 87 | } 88 | 89 | if info.ShortName != 0 { 90 | ret.hasShort = true 91 | } 92 | 93 | if len(info.ValueName) > 0 { 94 | ret.hasValueName = true 95 | } 96 | 97 | l := info.LongNameWithNamespace() + info.ValueName 98 | 99 | if len(info.Choices) != 0 { 100 | l += "[" + strings.Join(info.Choices, "|") + "]" 101 | } 102 | 103 | ret.updateLen(l, c != p.Command) 104 | } 105 | }) 106 | 107 | return ret 108 | } 109 | 110 | func wrapText(s string, l int, prefix string) string { 111 | var ret string 112 | 113 | if l < 10 { 114 | l = 10 115 | } 116 | 117 | // Basic text wrapping of s at spaces to fit in l 118 | lines := strings.Split(s, "\n") 119 | 120 | for _, line := range lines { 121 | var retline string 122 | 123 | line = strings.TrimSpace(line) 124 | 125 | for len(line) > l { 126 | // Try to split on space 127 | suffix := "" 128 | 129 | pos := strings.LastIndex(line[:l], " ") 130 | 131 | if pos < 0 { 132 | pos = l - 1 133 | suffix = "-\n" 134 | } 135 | 136 | if len(retline) != 0 { 137 | retline += "\n" + prefix 138 | } 139 | 140 | retline += strings.TrimSpace(line[:pos]) + suffix 141 | line = strings.TrimSpace(line[pos:]) 142 | } 143 | 144 | if len(line) > 0 { 145 | if len(retline) != 0 { 146 | retline += "\n" + prefix 147 | } 148 | 149 | retline += line 150 | } 151 | 152 | if len(ret) > 0 { 153 | ret += "\n" 154 | 155 | if len(retline) > 0 { 156 | ret += prefix 157 | } 158 | } 159 | 160 | ret += retline 161 | } 162 | 163 | return ret 164 | } 165 | 166 | func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) { 167 | line := &bytes.Buffer{} 168 | 169 | prefix := paddingBeforeOption 170 | 171 | if info.indent { 172 | prefix += 4 173 | } 174 | 175 | if option.Hidden { 176 | return 177 | } 178 | 179 | line.WriteString(strings.Repeat(" ", prefix)) 180 | 181 | if option.ShortName != 0 { 182 | line.WriteRune(defaultShortOptDelimiter) 183 | line.WriteRune(option.ShortName) 184 | } else if info.hasShort { 185 | line.WriteString(" ") 186 | } 187 | 188 | descstart := info.descriptionStart() + paddingBeforeOption 189 | 190 | if len(option.LongName) > 0 { 191 | if option.ShortName != 0 { 192 | line.WriteString(", ") 193 | } else if info.hasShort { 194 | line.WriteString(" ") 195 | } 196 | 197 | line.WriteString(defaultLongOptDelimiter) 198 | line.WriteString(option.LongNameWithNamespace()) 199 | } 200 | 201 | if option.canArgument() { 202 | line.WriteRune(defaultNameArgDelimiter) 203 | 204 | if len(option.ValueName) > 0 { 205 | line.WriteString(option.ValueName) 206 | } 207 | 208 | if len(option.Choices) > 0 { 209 | line.WriteString("[" + strings.Join(option.Choices, "|") + "]") 210 | } 211 | } 212 | 213 | written := line.Len() 214 | line.WriteTo(writer) 215 | 216 | if option.Description != "" { 217 | dw := descstart - written 218 | writer.WriteString(strings.Repeat(" ", dw)) 219 | 220 | var def string 221 | 222 | if len(option.DefaultMask) != 0 { 223 | if option.DefaultMask != "-" { 224 | def = option.DefaultMask 225 | } 226 | } else { 227 | def = option.defaultLiteral 228 | } 229 | 230 | var envDef string 231 | if option.EnvKeyWithNamespace() != "" { 232 | var envPrintable string 233 | if runtime.GOOS == "windows" { 234 | envPrintable = "%" + option.EnvKeyWithNamespace() + "%" 235 | } else { 236 | envPrintable = "$" + option.EnvKeyWithNamespace() 237 | } 238 | envDef = fmt.Sprintf(" [%s]", envPrintable) 239 | } 240 | 241 | var desc string 242 | 243 | if def != "" { 244 | desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef) 245 | } else { 246 | desc = option.Description + envDef 247 | } 248 | 249 | writer.WriteString(wrapText(desc, 250 | info.terminalColumns-descstart, 251 | strings.Repeat(" ", descstart))) 252 | } 253 | 254 | writer.WriteString("\n") 255 | } 256 | 257 | func maxCommandLength(s []*Command) int { 258 | if len(s) == 0 { 259 | return 0 260 | } 261 | 262 | ret := len(s[0].Name) 263 | 264 | for _, v := range s[1:] { 265 | l := len(v.Name) 266 | 267 | if l > ret { 268 | ret = l 269 | } 270 | } 271 | 272 | return ret 273 | } 274 | 275 | // WriteHelp writes a help message containing all the possible options and 276 | // their descriptions to the provided writer. Note that the HelpFlag parser 277 | // option provides a convenient way to add a -h/--help option group to the 278 | // command line parser which will automatically show the help messages using 279 | // this method. 280 | func (p *Parser) WriteHelp(writer io.Writer) { 281 | if writer == nil { 282 | return 283 | } 284 | 285 | wr := bufio.NewWriter(writer) 286 | aligninfo := p.getAlignmentInfo() 287 | 288 | cmd := p.Command 289 | 290 | for cmd.Active != nil { 291 | cmd = cmd.Active 292 | } 293 | 294 | if p.Name != "" { 295 | wr.WriteString("Usage:\n") 296 | wr.WriteString(" ") 297 | 298 | allcmd := p.Command 299 | 300 | for allcmd != nil { 301 | var usage string 302 | 303 | if allcmd == p.Command { 304 | if len(p.Usage) != 0 { 305 | usage = p.Usage 306 | } else if p.Options&HelpFlag != 0 { 307 | usage = "[OPTIONS]" 308 | } 309 | } else if us, ok := allcmd.data.(Usage); ok { 310 | usage = us.Usage() 311 | } else if allcmd.hasHelpOptions() { 312 | usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name) 313 | } 314 | 315 | if len(usage) != 0 { 316 | fmt.Fprintf(wr, " %s %s", allcmd.Name, usage) 317 | } else { 318 | fmt.Fprintf(wr, " %s", allcmd.Name) 319 | } 320 | 321 | if len(allcmd.args) > 0 { 322 | fmt.Fprintf(wr, " ") 323 | } 324 | 325 | for i, arg := range allcmd.args { 326 | if i != 0 { 327 | fmt.Fprintf(wr, " ") 328 | } 329 | 330 | name := arg.Name 331 | 332 | if arg.isRemaining() { 333 | name = name + "..." 334 | } 335 | 336 | if !allcmd.ArgsRequired { 337 | if arg.Required > 0 { 338 | fmt.Fprintf(wr, "%s", name) 339 | } else { 340 | fmt.Fprintf(wr, "[%s]", name) 341 | } 342 | } else { 343 | fmt.Fprintf(wr, "%s", name) 344 | } 345 | } 346 | 347 | if allcmd.Active == nil && len(allcmd.commands) > 0 { 348 | var co, cc string 349 | 350 | if allcmd.SubcommandsOptional { 351 | co, cc = "[", "]" 352 | } else { 353 | co, cc = "<", ">" 354 | } 355 | 356 | visibleCommands := allcmd.visibleCommands() 357 | 358 | if len(visibleCommands) > 3 { 359 | fmt.Fprintf(wr, " %scommand%s", co, cc) 360 | } else { 361 | subcommands := allcmd.sortedVisibleCommands() 362 | names := make([]string, len(subcommands)) 363 | 364 | for i, subc := range subcommands { 365 | names[i] = subc.Name 366 | } 367 | 368 | fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc) 369 | } 370 | } 371 | 372 | allcmd = allcmd.Active 373 | } 374 | 375 | fmt.Fprintln(wr) 376 | 377 | if len(cmd.LongDescription) != 0 { 378 | fmt.Fprintln(wr) 379 | 380 | t := wrapText(cmd.LongDescription, 381 | aligninfo.terminalColumns, 382 | "") 383 | 384 | fmt.Fprintln(wr, t) 385 | } 386 | } 387 | 388 | c := p.Command 389 | 390 | for c != nil { 391 | printcmd := c != p.Command 392 | 393 | c.eachGroup(func(grp *Group) { 394 | first := true 395 | 396 | // Skip built-in help group for all commands except the top-level 397 | // parser 398 | if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) { 399 | return 400 | } 401 | 402 | for _, info := range grp.options { 403 | if !info.showInHelp() { 404 | continue 405 | } 406 | 407 | if printcmd { 408 | fmt.Fprintf(wr, "\n[%s command options]\n", c.Name) 409 | aligninfo.indent = true 410 | printcmd = false 411 | } 412 | 413 | if first && cmd.Group != grp { 414 | fmt.Fprintln(wr) 415 | 416 | if aligninfo.indent { 417 | wr.WriteString(" ") 418 | } 419 | 420 | fmt.Fprintf(wr, "%s:\n", grp.ShortDescription) 421 | first = false 422 | } 423 | 424 | p.writeHelpOption(wr, info, aligninfo) 425 | } 426 | }) 427 | 428 | var args []*Arg 429 | for _, arg := range c.args { 430 | if arg.Description != "" { 431 | args = append(args, arg) 432 | } 433 | } 434 | 435 | if len(args) > 0 { 436 | if c == p.Command { 437 | fmt.Fprintf(wr, "\nArguments:\n") 438 | } else { 439 | fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name) 440 | } 441 | 442 | descStart := aligninfo.descriptionStart() + paddingBeforeOption 443 | 444 | for _, arg := range args { 445 | argPrefix := strings.Repeat(" ", paddingBeforeOption) 446 | argPrefix += arg.Name 447 | 448 | if len(arg.Description) > 0 { 449 | argPrefix += ":" 450 | wr.WriteString(argPrefix) 451 | 452 | // Space between "arg:" and the description start 453 | descPadding := strings.Repeat(" ", descStart-len(argPrefix)) 454 | // How much space the description gets before wrapping 455 | descWidth := aligninfo.terminalColumns - 1 - descStart 456 | // Whitespace to which we can indent new description lines 457 | descPrefix := strings.Repeat(" ", descStart) 458 | 459 | wr.WriteString(descPadding) 460 | wr.WriteString(wrapText(arg.Description, descWidth, descPrefix)) 461 | } else { 462 | wr.WriteString(argPrefix) 463 | } 464 | 465 | fmt.Fprintln(wr) 466 | } 467 | } 468 | 469 | c = c.Active 470 | } 471 | 472 | scommands := cmd.sortedVisibleCommands() 473 | 474 | if len(scommands) > 0 { 475 | maxnamelen := maxCommandLength(scommands) 476 | 477 | fmt.Fprintln(wr) 478 | fmt.Fprintln(wr, "Available commands:") 479 | 480 | for _, c := range scommands { 481 | fmt.Fprintf(wr, " %s", c.Name) 482 | 483 | if len(c.ShortDescription) > 0 { 484 | pad := strings.Repeat(" ", maxnamelen-len(c.Name)) 485 | fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) 486 | 487 | if len(c.Aliases) > 0 { 488 | fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", ")) 489 | } 490 | 491 | } 492 | 493 | fmt.Fprintln(wr) 494 | } 495 | } 496 | 497 | wr.Flush() 498 | } 499 | 500 | // WroteHelp is a helper to test the error from ParseArgs() to 501 | // determine if the help message was written. It is safe to 502 | // call without first checking that error is nil. 503 | func WroteHelp(err error) bool { 504 | if err == nil { // No error 505 | return false 506 | } 507 | 508 | flagError, ok := err.(*Error) 509 | if !ok { // Not a go-flag error 510 | return false 511 | } 512 | 513 | if flagError.Type != ErrHelp { // Did not print the help message 514 | return false 515 | } 516 | 517 | return true 518 | } 519 | -------------------------------------------------------------------------------- /ini.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "reflect" 9 | "sort" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // IniError contains location information on where an error occurred. 15 | type IniError struct { 16 | // The error message. 17 | Message string 18 | 19 | // The filename of the file in which the error occurred. 20 | File string 21 | 22 | // The line number at which the error occurred. 23 | LineNumber uint 24 | } 25 | 26 | // Error provides a "file:line: message" formatted message of the ini error. 27 | func (x *IniError) Error() string { 28 | return fmt.Sprintf( 29 | "%s:%d: %s", 30 | x.File, 31 | x.LineNumber, 32 | x.Message, 33 | ) 34 | } 35 | 36 | // IniOptions for writing 37 | type IniOptions uint 38 | 39 | const ( 40 | // IniNone indicates no options. 41 | IniNone IniOptions = 0 42 | 43 | // IniIncludeDefaults indicates that default values should be written. 44 | IniIncludeDefaults = 1 << iota 45 | 46 | // IniCommentDefaults indicates that if IniIncludeDefaults is used 47 | // options with default values are written but commented out. 48 | IniCommentDefaults 49 | 50 | // IniIncludeComments indicates that comments containing the description 51 | // of an option should be written. 52 | IniIncludeComments 53 | 54 | // IniDefault provides a default set of options. 55 | IniDefault = IniIncludeComments 56 | ) 57 | 58 | // IniParser is a utility to read and write flags options from and to ini 59 | // formatted strings. 60 | type IniParser struct { 61 | ParseAsDefaults bool // override default flags 62 | 63 | parser *Parser 64 | } 65 | 66 | type iniValue struct { 67 | Name string 68 | Value string 69 | Quoted bool 70 | LineNumber uint 71 | } 72 | 73 | type iniSection []iniValue 74 | 75 | type ini struct { 76 | File string 77 | Sections map[string]iniSection 78 | } 79 | 80 | // NewIniParser creates a new ini parser for a given Parser. 81 | func NewIniParser(p *Parser) *IniParser { 82 | return &IniParser{ 83 | parser: p, 84 | } 85 | } 86 | 87 | // IniParse is a convenience function to parse command line options with default 88 | // settings from an ini formatted file. The provided data is a pointer to a struct 89 | // representing the default option group (named "Application Options"). For 90 | // more control, use flags.NewParser. 91 | func IniParse(filename string, data interface{}) error { 92 | p := NewParser(data, Default) 93 | 94 | return NewIniParser(p).ParseFile(filename) 95 | } 96 | 97 | // ParseFile parses flags from an ini formatted file. See Parse for more 98 | // information on the ini file format. The returned errors can be of the type 99 | // flags.Error or flags.IniError. 100 | func (i *IniParser) ParseFile(filename string) error { 101 | ini, err := readIniFromFile(filename) 102 | 103 | if err != nil { 104 | return err 105 | } 106 | 107 | return i.parse(ini) 108 | } 109 | 110 | // Parse parses flags from an ini format. You can use ParseFile as a 111 | // convenience function to parse from a filename instead of a general 112 | // io.Reader. 113 | // 114 | // The format of the ini file is as follows: 115 | // 116 | // [Option group name] 117 | // option = value 118 | // 119 | // Each section in the ini file represents an option group or command in the 120 | // flags parser. The default flags parser option group (i.e. when using 121 | // flags.Parse) is named 'Application Options'. The ini option name is matched 122 | // in the following order: 123 | // 124 | // 1. Compared to the ini-name tag on the option struct field (if present) 125 | // 2. Compared to the struct field name 126 | // 3. Compared to the option long name (if present) 127 | // 4. Compared to the option short name (if present) 128 | // 129 | // Sections for nested groups and commands can be addressed using a dot `.' 130 | // namespacing notation (i.e [subcommand.Options]). Group section names are 131 | // matched case insensitive. 132 | // 133 | // The returned errors can be of the type flags.Error or flags.IniError. 134 | func (i *IniParser) Parse(reader io.Reader) error { 135 | ini, err := readIni(reader, "") 136 | 137 | if err != nil { 138 | return err 139 | } 140 | 141 | return i.parse(ini) 142 | } 143 | 144 | // WriteFile writes the flags as ini format into a file. See Write 145 | // for more information. The returned error occurs when the specified file 146 | // could not be opened for writing. 147 | func (i *IniParser) WriteFile(filename string, options IniOptions) error { 148 | return writeIniToFile(i, filename, options) 149 | } 150 | 151 | // Write writes the current values of all the flags to an ini format. 152 | // See Parse for more information on the ini file format. You typically 153 | // call this only after settings have been parsed since the default values of each 154 | // option are stored just before parsing the flags (this is only relevant when 155 | // IniIncludeDefaults is _not_ set in options). 156 | func (i *IniParser) Write(writer io.Writer, options IniOptions) { 157 | writeIni(i, writer, options) 158 | } 159 | 160 | func readFullLine(reader *bufio.Reader) (string, error) { 161 | var line []byte 162 | 163 | for { 164 | l, more, err := reader.ReadLine() 165 | 166 | if err != nil { 167 | return "", err 168 | } 169 | 170 | if line == nil && !more { 171 | return string(l), nil 172 | } 173 | 174 | line = append(line, l...) 175 | 176 | if !more { 177 | break 178 | } 179 | } 180 | 181 | return string(line), nil 182 | } 183 | 184 | func optionIniName(option *Option) string { 185 | name := option.tag.Get("_read-ini-name") 186 | 187 | if len(name) != 0 { 188 | return name 189 | } 190 | 191 | name = option.tag.Get("ini-name") 192 | 193 | if len(name) != 0 { 194 | return name 195 | } 196 | 197 | return option.field.Name 198 | } 199 | 200 | func writeGroupIni(cmd *Command, group *Group, namespace string, writer io.Writer, options IniOptions) { 201 | var sname string 202 | 203 | if len(namespace) != 0 { 204 | sname = namespace 205 | } 206 | 207 | if cmd.Group != group && len(group.ShortDescription) != 0 { 208 | if len(sname) != 0 { 209 | sname += "." 210 | } 211 | 212 | sname += group.ShortDescription 213 | } 214 | 215 | sectionwritten := false 216 | comments := (options & IniIncludeComments) != IniNone 217 | 218 | for _, option := range group.options { 219 | if option.isFunc() || option.Hidden { 220 | continue 221 | } 222 | 223 | if len(option.tag.Get("no-ini")) != 0 { 224 | continue 225 | } 226 | 227 | val := option.value 228 | 229 | if (options&IniIncludeDefaults) == IniNone && option.valueIsDefault() { 230 | continue 231 | } 232 | 233 | if !sectionwritten { 234 | fmt.Fprintf(writer, "[%s]\n", sname) 235 | sectionwritten = true 236 | } 237 | 238 | if comments && len(option.Description) != 0 { 239 | fmt.Fprintf(writer, "; %s\n", option.Description) 240 | } 241 | 242 | oname := optionIniName(option) 243 | 244 | commentOption := (options&(IniIncludeDefaults|IniCommentDefaults)) == IniIncludeDefaults|IniCommentDefaults && option.valueIsDefault() 245 | 246 | kind := val.Type().Kind() 247 | switch kind { 248 | case reflect.Slice: 249 | kind = val.Type().Elem().Kind() 250 | 251 | if val.Len() == 0 { 252 | writeOption(writer, oname, kind, "", "", true, option.iniQuote) 253 | } else { 254 | for idx := 0; idx < val.Len(); idx++ { 255 | v, _ := convertToString(val.Index(idx), option.tag) 256 | 257 | writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) 258 | } 259 | } 260 | case reflect.Map: 261 | kind = val.Type().Elem().Kind() 262 | 263 | if val.Len() == 0 { 264 | writeOption(writer, oname, kind, "", "", true, option.iniQuote) 265 | } else { 266 | mkeys := val.MapKeys() 267 | keys := make([]string, len(val.MapKeys())) 268 | kkmap := make(map[string]reflect.Value) 269 | 270 | for i, k := range mkeys { 271 | keys[i], _ = convertToString(k, option.tag) 272 | kkmap[keys[i]] = k 273 | } 274 | 275 | sort.Strings(keys) 276 | 277 | for _, k := range keys { 278 | v, _ := convertToString(val.MapIndex(kkmap[k]), option.tag) 279 | 280 | writeOption(writer, oname, kind, k, v, commentOption, option.iniQuote) 281 | } 282 | } 283 | default: 284 | v, _ := convertToString(val, option.tag) 285 | 286 | writeOption(writer, oname, kind, "", v, commentOption, option.iniQuote) 287 | } 288 | 289 | if comments { 290 | fmt.Fprintln(writer) 291 | } 292 | } 293 | 294 | if sectionwritten && !comments { 295 | fmt.Fprintln(writer) 296 | } 297 | } 298 | 299 | func writeOption(writer io.Writer, optionName string, optionType reflect.Kind, optionKey string, optionValue string, commentOption bool, forceQuote bool) { 300 | if forceQuote || (optionType == reflect.String && !isPrint(optionValue)) { 301 | optionValue = strconv.Quote(optionValue) 302 | } 303 | 304 | comment := "" 305 | if commentOption { 306 | comment = "; " 307 | } 308 | 309 | fmt.Fprintf(writer, "%s%s =", comment, optionName) 310 | 311 | if optionKey != "" { 312 | fmt.Fprintf(writer, " %s:%s", optionKey, optionValue) 313 | } else if optionValue != "" { 314 | fmt.Fprintf(writer, " %s", optionValue) 315 | } 316 | 317 | fmt.Fprintln(writer) 318 | } 319 | 320 | func writeCommandIni(command *Command, namespace string, writer io.Writer, options IniOptions) { 321 | command.eachGroup(func(group *Group) { 322 | if !group.Hidden { 323 | writeGroupIni(command, group, namespace, writer, options) 324 | } 325 | }) 326 | 327 | for _, c := range command.commands { 328 | var fqn string 329 | 330 | if c.Hidden { 331 | continue 332 | } 333 | 334 | if len(namespace) != 0 { 335 | fqn = namespace + "." + c.Name 336 | } else { 337 | fqn = c.Name 338 | } 339 | 340 | writeCommandIni(c, fqn, writer, options) 341 | } 342 | } 343 | 344 | func writeIni(parser *IniParser, writer io.Writer, options IniOptions) { 345 | writeCommandIni(parser.parser.Command, "", writer, options) 346 | } 347 | 348 | func writeIniToFile(parser *IniParser, filename string, options IniOptions) error { 349 | file, err := os.Create(filename) 350 | 351 | if err != nil { 352 | return err 353 | } 354 | 355 | defer file.Close() 356 | 357 | writeIni(parser, file, options) 358 | 359 | return nil 360 | } 361 | 362 | func readIniFromFile(filename string) (*ini, error) { 363 | file, err := os.Open(filename) 364 | 365 | if err != nil { 366 | return nil, err 367 | } 368 | 369 | defer file.Close() 370 | 371 | return readIni(file, filename) 372 | } 373 | 374 | func readIni(contents io.Reader, filename string) (*ini, error) { 375 | ret := &ini{ 376 | File: filename, 377 | Sections: make(map[string]iniSection), 378 | } 379 | 380 | reader := bufio.NewReader(contents) 381 | 382 | // Empty global section 383 | section := make(iniSection, 0, 10) 384 | sectionname := "" 385 | 386 | ret.Sections[sectionname] = section 387 | 388 | var lineno uint 389 | 390 | for { 391 | line, err := readFullLine(reader) 392 | 393 | if err == io.EOF { 394 | break 395 | } else if err != nil { 396 | return nil, err 397 | } 398 | 399 | lineno++ 400 | line = strings.TrimSpace(line) 401 | 402 | // Skip empty lines and lines starting with ; (comments) 403 | if len(line) == 0 || line[0] == ';' || line[0] == '#' { 404 | continue 405 | } 406 | 407 | if line[0] == '[' { 408 | if line[0] != '[' || line[len(line)-1] != ']' { 409 | return nil, &IniError{ 410 | Message: "malformed section header", 411 | File: filename, 412 | LineNumber: lineno, 413 | } 414 | } 415 | 416 | name := strings.TrimSpace(line[1 : len(line)-1]) 417 | 418 | if len(name) == 0 { 419 | return nil, &IniError{ 420 | Message: "empty section name", 421 | File: filename, 422 | LineNumber: lineno, 423 | } 424 | } 425 | 426 | sectionname = name 427 | section = ret.Sections[name] 428 | 429 | if section == nil { 430 | section = make(iniSection, 0, 10) 431 | ret.Sections[name] = section 432 | } 433 | 434 | continue 435 | } 436 | 437 | // Parse option here 438 | keyval := strings.SplitN(line, "=", 2) 439 | 440 | if len(keyval) != 2 { 441 | return nil, &IniError{ 442 | Message: fmt.Sprintf("malformed key=value (%s)", line), 443 | File: filename, 444 | LineNumber: lineno, 445 | } 446 | } 447 | 448 | name := strings.TrimSpace(keyval[0]) 449 | value := strings.TrimSpace(keyval[1]) 450 | quoted := false 451 | 452 | if len(value) != 0 && value[0] == '"' { 453 | if v, err := strconv.Unquote(value); err == nil { 454 | value = v 455 | 456 | quoted = true 457 | } else { 458 | return nil, &IniError{ 459 | Message: err.Error(), 460 | File: filename, 461 | LineNumber: lineno, 462 | } 463 | } 464 | } 465 | 466 | section = append(section, iniValue{ 467 | Name: name, 468 | Value: value, 469 | Quoted: quoted, 470 | LineNumber: lineno, 471 | }) 472 | 473 | ret.Sections[sectionname] = section 474 | } 475 | 476 | return ret, nil 477 | } 478 | 479 | func (i *IniParser) matchingGroups(name string) []*Group { 480 | if len(name) == 0 { 481 | var ret []*Group 482 | 483 | i.parser.eachGroup(func(g *Group) { 484 | ret = append(ret, g) 485 | }) 486 | 487 | return ret 488 | } 489 | 490 | g := i.parser.groupByName(name) 491 | 492 | if g != nil { 493 | return []*Group{g} 494 | } 495 | 496 | return nil 497 | } 498 | 499 | func (i *IniParser) parse(ini *ini) error { 500 | p := i.parser 501 | 502 | p.eachOption(func(cmd *Command, group *Group, option *Option) { 503 | option.clearReferenceBeforeSet = true 504 | }) 505 | 506 | var quotesLookup = make(map[*Option]bool) 507 | 508 | for name, section := range ini.Sections { 509 | groups := i.matchingGroups(name) 510 | 511 | if len(groups) == 0 { 512 | if (p.Options & IgnoreUnknown) == None { 513 | return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) 514 | } 515 | 516 | continue 517 | } 518 | 519 | for _, inival := range section { 520 | var opt *Option 521 | 522 | for _, group := range groups { 523 | opt = group.optionByName(inival.Name, func(o *Option, n string) bool { 524 | return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n) 525 | }) 526 | 527 | if opt != nil && len(opt.tag.Get("no-ini")) != 0 { 528 | opt = nil 529 | } 530 | 531 | if opt != nil { 532 | break 533 | } 534 | } 535 | 536 | if opt == nil { 537 | if (p.Options & IgnoreUnknown) == None { 538 | return &IniError{ 539 | Message: fmt.Sprintf("unknown option: %s", inival.Name), 540 | File: ini.File, 541 | LineNumber: inival.LineNumber, 542 | } 543 | } 544 | 545 | continue 546 | } 547 | 548 | // ini value is ignored if parsed as default but defaults are prevented 549 | if i.ParseAsDefaults && opt.preventDefault { 550 | continue 551 | } 552 | 553 | pval := &inival.Value 554 | 555 | if !opt.canArgument() && len(inival.Value) == 0 { 556 | pval = nil 557 | } else { 558 | if opt.value.Type().Kind() == reflect.Map { 559 | parts := strings.SplitN(inival.Value, ":", 2) 560 | 561 | // only handle unquoting 562 | if len(parts) == 2 && parts[1][0] == '"' { 563 | if v, err := strconv.Unquote(parts[1]); err == nil { 564 | parts[1] = v 565 | 566 | inival.Quoted = true 567 | } else { 568 | return &IniError{ 569 | Message: err.Error(), 570 | File: ini.File, 571 | LineNumber: inival.LineNumber, 572 | } 573 | } 574 | 575 | s := parts[0] + ":" + parts[1] 576 | 577 | pval = &s 578 | } 579 | } 580 | } 581 | 582 | var err error 583 | 584 | if i.ParseAsDefaults { 585 | err = opt.setDefault(pval) 586 | } else { 587 | err = opt.Set(pval) 588 | } 589 | 590 | if err != nil { 591 | return &IniError{ 592 | Message: err.Error(), 593 | File: ini.File, 594 | LineNumber: inival.LineNumber, 595 | } 596 | } 597 | 598 | // Defaults from ini files take precendence over defaults from parser 599 | opt.preventDefault = true 600 | 601 | // either all INI values are quoted or only values who need quoting 602 | if _, ok := quotesLookup[opt]; !inival.Quoted || !ok { 603 | quotesLookup[opt] = inival.Quoted 604 | } 605 | 606 | opt.tag.Set("_read-ini-name", inival.Name) 607 | } 608 | } 609 | 610 | for opt, quoted := range quotesLookup { 611 | opt.iniQuote = quoted 612 | } 613 | 614 | return nil 615 | } 616 | -------------------------------------------------------------------------------- /long_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLong(t *testing.T) { 8 | var opts = struct { 9 | Value bool `long:"value"` 10 | }{} 11 | 12 | ret := assertParseSuccess(t, &opts, "--value") 13 | 14 | assertStringArray(t, ret, []string{}) 15 | 16 | if !opts.Value { 17 | t.Errorf("Expected Value to be true") 18 | } 19 | } 20 | 21 | func TestLongArg(t *testing.T) { 22 | var opts = struct { 23 | Value string `long:"value"` 24 | }{} 25 | 26 | ret := assertParseSuccess(t, &opts, "--value", "value") 27 | 28 | assertStringArray(t, ret, []string{}) 29 | assertString(t, opts.Value, "value") 30 | } 31 | 32 | func TestLongArgEqual(t *testing.T) { 33 | var opts = struct { 34 | Value string `long:"value"` 35 | }{} 36 | 37 | ret := assertParseSuccess(t, &opts, "--value=value") 38 | 39 | assertStringArray(t, ret, []string{}) 40 | assertString(t, opts.Value, "value") 41 | } 42 | 43 | func TestLongDefault(t *testing.T) { 44 | var opts = struct { 45 | Value string `long:"value" default:"value"` 46 | }{} 47 | 48 | ret := assertParseSuccess(t, &opts) 49 | 50 | assertStringArray(t, ret, []string{}) 51 | assertString(t, opts.Value, "value") 52 | } 53 | 54 | func TestLongOptional(t *testing.T) { 55 | var opts = struct { 56 | Value string `long:"value" optional:"yes" optional-value:"value"` 57 | }{} 58 | 59 | ret := assertParseSuccess(t, &opts, "--value") 60 | 61 | assertStringArray(t, ret, []string{}) 62 | assertString(t, opts.Value, "value") 63 | } 64 | 65 | func TestLongOptionalArg(t *testing.T) { 66 | var opts = struct { 67 | Value string `long:"value" optional:"yes" optional-value:"value"` 68 | }{} 69 | 70 | ret := assertParseSuccess(t, &opts, "--value", "no") 71 | 72 | assertStringArray(t, ret, []string{"no"}) 73 | assertString(t, opts.Value, "value") 74 | } 75 | 76 | func TestLongOptionalArgEqual(t *testing.T) { 77 | var opts = struct { 78 | Value string `long:"value" optional:"yes" optional-value:"value"` 79 | }{} 80 | 81 | ret := assertParseSuccess(t, &opts, "--value=value", "no") 82 | 83 | assertStringArray(t, ret, []string{"no"}) 84 | assertString(t, opts.Value, "value") 85 | } 86 | -------------------------------------------------------------------------------- /man.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func manQuoteLines(s string) string { 14 | lines := strings.Split(s, "\n") 15 | parts := []string{} 16 | 17 | for _, line := range lines { 18 | parts = append(parts, manQuote(line)) 19 | } 20 | 21 | return strings.Join(parts, "\n") 22 | } 23 | 24 | func manQuote(s string) string { 25 | return strings.Replace(s, "\\", "\\\\", -1) 26 | } 27 | 28 | func formatForMan(wr io.Writer, s string, quoter func(s string) string) { 29 | for { 30 | idx := strings.IndexRune(s, '`') 31 | 32 | if idx < 0 { 33 | fmt.Fprintf(wr, "%s", quoter(s)) 34 | break 35 | } 36 | 37 | fmt.Fprintf(wr, "%s", quoter(s[:idx])) 38 | 39 | s = s[idx+1:] 40 | idx = strings.IndexRune(s, '\'') 41 | 42 | if idx < 0 { 43 | fmt.Fprintf(wr, "%s", quoter(s)) 44 | break 45 | } 46 | 47 | fmt.Fprintf(wr, "\\fB%s\\fP", quoter(s[:idx])) 48 | s = s[idx+1:] 49 | } 50 | } 51 | 52 | func writeManPageOptions(wr io.Writer, grp *Group) { 53 | grp.eachGroup(func(group *Group) { 54 | if !group.showInHelp() { 55 | return 56 | } 57 | 58 | // If the parent (grp) has any subgroups, display their descriptions as 59 | // subsection headers similar to the output of --help. 60 | if group.ShortDescription != "" && len(grp.groups) > 0 { 61 | fmt.Fprintf(wr, ".SS %s\n", group.ShortDescription) 62 | 63 | if group.LongDescription != "" { 64 | formatForMan(wr, group.LongDescription, manQuoteLines) 65 | fmt.Fprintln(wr, "") 66 | } 67 | } 68 | 69 | for _, opt := range group.options { 70 | if !opt.showInHelp() { 71 | continue 72 | } 73 | 74 | fmt.Fprintln(wr, ".TP") 75 | fmt.Fprintf(wr, "\\fB") 76 | 77 | if opt.ShortName != 0 { 78 | fmt.Fprintf(wr, "\\fB\\-%c\\fR", opt.ShortName) 79 | } 80 | 81 | if len(opt.LongName) != 0 { 82 | if opt.ShortName != 0 { 83 | fmt.Fprintf(wr, ", ") 84 | } 85 | 86 | fmt.Fprintf(wr, "\\fB\\-\\-%s\\fR", manQuote(opt.LongNameWithNamespace())) 87 | } 88 | 89 | if len(opt.ValueName) != 0 || opt.OptionalArgument { 90 | if opt.OptionalArgument { 91 | fmt.Fprintf(wr, " [\\fI%s=%s\\fR]", manQuote(opt.ValueName), manQuote(strings.Join(quoteV(opt.OptionalValue), ", "))) 92 | } else { 93 | fmt.Fprintf(wr, " \\fI%s\\fR", manQuote(opt.ValueName)) 94 | } 95 | } 96 | 97 | if len(opt.Default) != 0 { 98 | fmt.Fprintf(wr, " ", manQuote(strings.Join(quoteV(opt.Default), ", "))) 99 | } else if len(opt.EnvKeyWithNamespace()) != 0 { 100 | if runtime.GOOS == "windows" { 101 | fmt.Fprintf(wr, " ", manQuote(opt.EnvKeyWithNamespace())) 102 | } else { 103 | fmt.Fprintf(wr, " ", manQuote(opt.EnvKeyWithNamespace())) 104 | } 105 | } 106 | 107 | if opt.Required { 108 | fmt.Fprintf(wr, " (\\fIrequired\\fR)") 109 | } 110 | 111 | fmt.Fprintln(wr, "\\fP") 112 | 113 | if len(opt.Description) != 0 { 114 | formatForMan(wr, opt.Description, manQuoteLines) 115 | fmt.Fprintln(wr, "") 116 | } 117 | } 118 | }) 119 | } 120 | 121 | func writeManPageSubcommands(wr io.Writer, name string, usagePrefix string, root *Command) { 122 | commands := root.sortedVisibleCommands() 123 | 124 | for _, c := range commands { 125 | var nn string 126 | 127 | if c.Hidden { 128 | continue 129 | } 130 | 131 | if len(name) != 0 { 132 | nn = name + " " + c.Name 133 | } else { 134 | nn = c.Name 135 | } 136 | 137 | writeManPageCommand(wr, nn, usagePrefix, c) 138 | } 139 | } 140 | 141 | func writeManPageCommand(wr io.Writer, name string, usagePrefix string, command *Command) { 142 | fmt.Fprintf(wr, ".SS %s\n", name) 143 | fmt.Fprintln(wr, command.ShortDescription) 144 | 145 | if len(command.LongDescription) > 0 { 146 | fmt.Fprintln(wr, "") 147 | 148 | cmdstart := fmt.Sprintf("The %s command", manQuote(command.Name)) 149 | 150 | if strings.HasPrefix(command.LongDescription, cmdstart) { 151 | fmt.Fprintf(wr, "The \\fI%s\\fP command", manQuote(command.Name)) 152 | 153 | formatForMan(wr, command.LongDescription[len(cmdstart):], manQuoteLines) 154 | fmt.Fprintln(wr, "") 155 | } else { 156 | formatForMan(wr, command.LongDescription, manQuoteLines) 157 | fmt.Fprintln(wr, "") 158 | } 159 | } 160 | 161 | var pre = usagePrefix + " " + command.Name 162 | 163 | var usage string 164 | if us, ok := command.data.(Usage); ok { 165 | usage = us.Usage() 166 | } else if command.hasHelpOptions() { 167 | usage = fmt.Sprintf("[%s-OPTIONS]", command.Name) 168 | } 169 | 170 | var nextPrefix = pre 171 | if len(usage) > 0 { 172 | fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n.TP\n", manQuote(pre), manQuote(usage)) 173 | nextPrefix = pre + " " + usage 174 | } 175 | 176 | if len(command.Aliases) > 0 { 177 | fmt.Fprintf(wr, "\n\\fBAliases\\fP: %s\n\n", manQuote(strings.Join(command.Aliases, ", "))) 178 | } 179 | 180 | writeManPageOptions(wr, command.Group) 181 | writeManPageSubcommands(wr, name, nextPrefix, command) 182 | } 183 | 184 | // WriteManPage writes a basic man page in groff format to the specified 185 | // writer. 186 | func (p *Parser) WriteManPage(wr io.Writer) { 187 | t := time.Now() 188 | source_date_epoch := os.Getenv("SOURCE_DATE_EPOCH") 189 | if source_date_epoch != "" { 190 | sde, err := strconv.ParseInt(source_date_epoch, 10, 64) 191 | if err != nil { 192 | panic(fmt.Sprintf("Invalid SOURCE_DATE_EPOCH: %s", err)) 193 | } 194 | t = time.Unix(sde, 0) 195 | } 196 | 197 | fmt.Fprintf(wr, ".TH %s 1 \"%s\"\n", manQuote(p.Name), t.Format("2 January 2006")) 198 | fmt.Fprintln(wr, ".SH NAME") 199 | fmt.Fprintf(wr, "%s \\- %s\n", manQuote(p.Name), manQuoteLines(p.ShortDescription)) 200 | fmt.Fprintln(wr, ".SH SYNOPSIS") 201 | 202 | usage := p.Usage 203 | 204 | if len(usage) == 0 { 205 | usage = "[OPTIONS]" 206 | } 207 | 208 | fmt.Fprintf(wr, "\\fB%s\\fP %s\n", manQuote(p.Name), manQuote(usage)) 209 | fmt.Fprintln(wr, ".SH DESCRIPTION") 210 | 211 | formatForMan(wr, p.LongDescription, manQuoteLines) 212 | fmt.Fprintln(wr, "") 213 | 214 | fmt.Fprintln(wr, ".SH OPTIONS") 215 | 216 | writeManPageOptions(wr, p.Command.Group) 217 | 218 | if len(p.visibleCommands()) > 0 { 219 | fmt.Fprintln(wr, ".SH COMMANDS") 220 | 221 | writeManPageSubcommands(wr, "", p.Name+" "+usage, p.Command) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /marshal_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type marshalled string 9 | 10 | func (m *marshalled) UnmarshalFlag(value string) error { 11 | if value == "yes" { 12 | *m = "true" 13 | } else if value == "no" { 14 | *m = "false" 15 | } else { 16 | return fmt.Errorf("`%s' is not a valid value, please specify `yes' or `no'", value) 17 | } 18 | 19 | return nil 20 | } 21 | 22 | func (m marshalled) MarshalFlag() (string, error) { 23 | if m == "true" { 24 | return "yes", nil 25 | } 26 | 27 | return "no", nil 28 | } 29 | 30 | type marshalledError bool 31 | 32 | func (m marshalledError) MarshalFlag() (string, error) { 33 | return "", newErrorf(ErrMarshal, "Failed to marshal") 34 | } 35 | 36 | func TestUnmarshal(t *testing.T) { 37 | var opts = struct { 38 | Value marshalled `short:"v"` 39 | }{} 40 | 41 | ret := assertParseSuccess(t, &opts, "-v=yes") 42 | 43 | assertStringArray(t, ret, []string{}) 44 | 45 | if opts.Value != "true" { 46 | t.Errorf("Expected Value to be \"true\"") 47 | } 48 | } 49 | 50 | func TestUnmarshalDefault(t *testing.T) { 51 | var opts = struct { 52 | Value marshalled `short:"v" default:"yes"` 53 | }{} 54 | 55 | ret := assertParseSuccess(t, &opts) 56 | 57 | assertStringArray(t, ret, []string{}) 58 | 59 | if opts.Value != "true" { 60 | t.Errorf("Expected Value to be \"true\"") 61 | } 62 | } 63 | 64 | func TestUnmarshalOptional(t *testing.T) { 65 | var opts = struct { 66 | Value marshalled `short:"v" optional:"yes" optional-value:"yes"` 67 | }{} 68 | 69 | ret := assertParseSuccess(t, &opts, "-v") 70 | 71 | assertStringArray(t, ret, []string{}) 72 | 73 | if opts.Value != "true" { 74 | t.Errorf("Expected Value to be \"true\"") 75 | } 76 | } 77 | 78 | func TestUnmarshalError(t *testing.T) { 79 | var opts = struct { 80 | Value marshalled `short:"v"` 81 | }{} 82 | 83 | assertParseFail(t, ErrMarshal, fmt.Sprintf("invalid argument for flag `%cv' (expected flags.marshalled): `invalid' is not a valid value, please specify `yes' or `no'", defaultShortOptDelimiter), &opts, "-vinvalid") 84 | } 85 | 86 | func TestUnmarshalPositionalError(t *testing.T) { 87 | var opts = struct { 88 | Args struct { 89 | Value marshalled 90 | } `positional-args:"yes"` 91 | }{} 92 | 93 | parser := NewParser(&opts, Default&^PrintErrors) 94 | _, err := parser.ParseArgs([]string{"invalid"}) 95 | 96 | msg := "`invalid' is not a valid value, please specify `yes' or `no'" 97 | 98 | if err == nil { 99 | assertFatalf(t, "Expected error: %s", msg) 100 | return 101 | } 102 | 103 | if err.Error() != msg { 104 | assertErrorf(t, "Expected error message %#v, but got %#v", msg, err.Error()) 105 | } 106 | } 107 | 108 | func TestMarshalError(t *testing.T) { 109 | var opts = struct { 110 | Value marshalledError `short:"v"` 111 | }{} 112 | 113 | p := NewParser(&opts, Default) 114 | o := p.Command.Groups()[0].Options()[0] 115 | 116 | _, err := convertToString(o.value, o.tag) 117 | 118 | assertError(t, err, ErrMarshal, "Failed to marshal") 119 | } 120 | -------------------------------------------------------------------------------- /multitag.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type multiTag struct { 8 | value string 9 | cache map[string][]string 10 | } 11 | 12 | func newMultiTag(v string) multiTag { 13 | return multiTag{ 14 | value: v, 15 | } 16 | } 17 | 18 | func (x *multiTag) scan() (map[string][]string, error) { 19 | v := x.value 20 | 21 | ret := make(map[string][]string) 22 | 23 | // This is mostly copied from reflect.StructTag.Get 24 | for v != "" { 25 | i := 0 26 | 27 | // Skip whitespace 28 | for i < len(v) && v[i] == ' ' { 29 | i++ 30 | } 31 | 32 | v = v[i:] 33 | 34 | if v == "" { 35 | break 36 | } 37 | 38 | // Scan to colon to find key 39 | i = 0 40 | 41 | for i < len(v) && v[i] != ' ' && v[i] != ':' && v[i] != '"' { 42 | i++ 43 | } 44 | 45 | if i >= len(v) { 46 | return nil, newErrorf(ErrTag, "expected `:' after key name, but got end of tag (in `%v`)", x.value) 47 | } 48 | 49 | if v[i] != ':' { 50 | return nil, newErrorf(ErrTag, "expected `:' after key name, but got `%v' (in `%v`)", v[i], x.value) 51 | } 52 | 53 | if i+1 >= len(v) { 54 | return nil, newErrorf(ErrTag, "expected `\"' to start tag value at end of tag (in `%v`)", x.value) 55 | } 56 | 57 | if v[i+1] != '"' { 58 | return nil, newErrorf(ErrTag, "expected `\"' to start tag value, but got `%v' (in `%v`)", v[i+1], x.value) 59 | } 60 | 61 | name := v[:i] 62 | v = v[i+1:] 63 | 64 | // Scan quoted string to find value 65 | i = 1 66 | 67 | for i < len(v) && v[i] != '"' { 68 | if v[i] == '\n' { 69 | return nil, newErrorf(ErrTag, "unexpected newline in tag value `%v' (in `%v`)", name, x.value) 70 | } 71 | 72 | if v[i] == '\\' { 73 | i++ 74 | } 75 | i++ 76 | } 77 | 78 | if i >= len(v) { 79 | return nil, newErrorf(ErrTag, "expected end of tag value `\"' at end of tag (in `%v`)", x.value) 80 | } 81 | 82 | val, err := strconv.Unquote(v[:i+1]) 83 | 84 | if err != nil { 85 | return nil, newErrorf(ErrTag, "Malformed value of tag `%v:%v` => %v (in `%v`)", name, v[:i+1], err, x.value) 86 | } 87 | 88 | v = v[i+1:] 89 | 90 | ret[name] = append(ret[name], val) 91 | } 92 | 93 | return ret, nil 94 | } 95 | 96 | func (x *multiTag) Parse() error { 97 | vals, err := x.scan() 98 | x.cache = vals 99 | 100 | return err 101 | } 102 | 103 | func (x *multiTag) cached() map[string][]string { 104 | if x.cache == nil { 105 | cache, _ := x.scan() 106 | 107 | if cache == nil { 108 | cache = make(map[string][]string) 109 | } 110 | 111 | x.cache = cache 112 | } 113 | 114 | return x.cache 115 | } 116 | 117 | func (x *multiTag) Get(key string) string { 118 | c := x.cached() 119 | 120 | if v, ok := c[key]; ok { 121 | return v[len(v)-1] 122 | } 123 | 124 | return "" 125 | } 126 | 127 | func (x *multiTag) GetMany(key string) []string { 128 | c := x.cached() 129 | return c[key] 130 | } 131 | 132 | func (x *multiTag) Set(key string, value string) { 133 | c := x.cached() 134 | c[key] = []string{value} 135 | } 136 | 137 | func (x *multiTag) SetMany(key string, value []string) { 138 | c := x.cached() 139 | c[key] = value 140 | } 141 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strings" 9 | "unicode/utf8" 10 | ) 11 | 12 | // Option flag information. Contains a description of the option, short and 13 | // long name as well as a default value and whether an argument for this 14 | // flag is optional. 15 | type Option struct { 16 | // The description of the option flag. This description is shown 17 | // automatically in the built-in help. 18 | Description string 19 | 20 | // The short name of the option (a single character). If not 0, the 21 | // option flag can be 'activated' using -. Either ShortName 22 | // or LongName needs to be non-empty. 23 | ShortName rune 24 | 25 | // The long name of the option. If not "", the option flag can be 26 | // activated using --. Either ShortName or LongName needs 27 | // to be non-empty. 28 | LongName string 29 | 30 | // The default value of the option. 31 | Default []string 32 | 33 | // The optional environment default value key name. 34 | EnvDefaultKey string 35 | 36 | // The optional delimiter string for EnvDefaultKey values. 37 | EnvDefaultDelim string 38 | 39 | // If true, specifies that the argument to an option flag is optional. 40 | // When no argument to the flag is specified on the command line, the 41 | // value of OptionalValue will be set in the field this option represents. 42 | // This is only valid for non-boolean options. 43 | OptionalArgument bool 44 | 45 | // The optional value of the option. The optional value is used when 46 | // the option flag is marked as having an OptionalArgument. This means 47 | // that when the flag is specified, but no option argument is given, 48 | // the value of the field this option represents will be set to 49 | // OptionalValue. This is only valid for non-boolean options. 50 | OptionalValue []string 51 | 52 | // If true, the option _must_ be specified on the command line. If the 53 | // option is not specified, the parser will generate an ErrRequired type 54 | // error. 55 | Required bool 56 | 57 | // A name for the value of an option shown in the Help as --flag [ValueName] 58 | ValueName string 59 | 60 | // A mask value to show in the help instead of the default value. This 61 | // is useful for hiding sensitive information in the help, such as 62 | // passwords. 63 | DefaultMask string 64 | 65 | // If non empty, only a certain set of values is allowed for an option. 66 | Choices []string 67 | 68 | // If true, the option is not displayed in the help or man page 69 | Hidden bool 70 | 71 | // The group which the option belongs to 72 | group *Group 73 | 74 | // The struct field which the option represents. 75 | field reflect.StructField 76 | 77 | // The struct field value which the option represents. 78 | value reflect.Value 79 | 80 | // Determines if the option will be always quoted in the INI output 81 | iniQuote bool 82 | 83 | tag multiTag 84 | isSet bool 85 | isSetDefault bool 86 | preventDefault bool 87 | clearReferenceBeforeSet bool 88 | 89 | defaultLiteral string 90 | } 91 | 92 | // LongNameWithNamespace returns the option's long name with the group namespaces 93 | // prepended by walking up the option's group tree. Namespaces and the long name 94 | // itself are separated by the parser's namespace delimiter. If the long name is 95 | // empty an empty string is returned. 96 | func (option *Option) LongNameWithNamespace() string { 97 | if len(option.LongName) == 0 { 98 | return "" 99 | } 100 | 101 | // fetch the namespace delimiter from the parser which is always at the 102 | // end of the group hierarchy 103 | namespaceDelimiter := "" 104 | g := option.group 105 | 106 | for { 107 | if p, ok := g.parent.(*Parser); ok { 108 | namespaceDelimiter = p.NamespaceDelimiter 109 | 110 | break 111 | } 112 | 113 | switch i := g.parent.(type) { 114 | case *Command: 115 | g = i.Group 116 | case *Group: 117 | g = i 118 | } 119 | } 120 | 121 | // concatenate long name with namespace 122 | longName := option.LongName 123 | g = option.group 124 | 125 | for g != nil { 126 | if g.Namespace != "" { 127 | longName = g.Namespace + namespaceDelimiter + longName 128 | } 129 | 130 | switch i := g.parent.(type) { 131 | case *Command: 132 | g = i.Group 133 | case *Group: 134 | g = i 135 | case *Parser: 136 | g = nil 137 | } 138 | } 139 | 140 | return longName 141 | } 142 | 143 | // EnvKeyWithNamespace returns the option's env key with the group namespaces 144 | // prepended by walking up the option's group tree. Namespaces and the env key 145 | // itself are separated by the parser's namespace delimiter. If the env key is 146 | // empty an empty string is returned. 147 | func (option *Option) EnvKeyWithNamespace() string { 148 | if len(option.EnvDefaultKey) == 0 { 149 | return "" 150 | } 151 | 152 | // fetch the namespace delimiter from the parser which is always at the 153 | // end of the group hierarchy 154 | namespaceDelimiter := "" 155 | g := option.group 156 | 157 | for { 158 | if p, ok := g.parent.(*Parser); ok { 159 | namespaceDelimiter = p.EnvNamespaceDelimiter 160 | 161 | break 162 | } 163 | 164 | switch i := g.parent.(type) { 165 | case *Command: 166 | g = i.Group 167 | case *Group: 168 | g = i 169 | } 170 | } 171 | 172 | // concatenate long name with namespace 173 | key := option.EnvDefaultKey 174 | g = option.group 175 | 176 | for g != nil { 177 | if g.EnvNamespace != "" { 178 | key = g.EnvNamespace + namespaceDelimiter + key 179 | } 180 | 181 | switch i := g.parent.(type) { 182 | case *Command: 183 | g = i.Group 184 | case *Group: 185 | g = i 186 | case *Parser: 187 | g = nil 188 | } 189 | } 190 | 191 | return key 192 | } 193 | 194 | // String converts an option to a human friendly readable string describing the 195 | // option. 196 | func (option *Option) String() string { 197 | var s string 198 | var short string 199 | 200 | if option.ShortName != 0 { 201 | data := make([]byte, utf8.RuneLen(option.ShortName)) 202 | utf8.EncodeRune(data, option.ShortName) 203 | short = string(data) 204 | 205 | if len(option.LongName) != 0 { 206 | s = fmt.Sprintf("%s%s, %s%s", 207 | string(defaultShortOptDelimiter), short, 208 | defaultLongOptDelimiter, option.LongNameWithNamespace()) 209 | } else { 210 | s = fmt.Sprintf("%s%s", string(defaultShortOptDelimiter), short) 211 | } 212 | } else if len(option.LongName) != 0 { 213 | s = fmt.Sprintf("%s%s", defaultLongOptDelimiter, option.LongNameWithNamespace()) 214 | } 215 | 216 | return s 217 | } 218 | 219 | // Value returns the option value as an interface{}. 220 | func (option *Option) Value() interface{} { 221 | return option.value.Interface() 222 | } 223 | 224 | // Field returns the reflect struct field of the option. 225 | func (option *Option) Field() reflect.StructField { 226 | return option.field 227 | } 228 | 229 | // IsSet returns true if option has been set 230 | func (option *Option) IsSet() bool { 231 | return option.isSet 232 | } 233 | 234 | // IsSetDefault returns true if option has been set via the default option tag 235 | func (option *Option) IsSetDefault() bool { 236 | return option.isSetDefault 237 | } 238 | 239 | // Set the value of an option to the specified value. An error will be returned 240 | // if the specified value could not be converted to the corresponding option 241 | // value type. 242 | func (option *Option) Set(value *string) error { 243 | kind := option.value.Type().Kind() 244 | 245 | if (kind == reflect.Map || kind == reflect.Slice) && option.clearReferenceBeforeSet { 246 | option.empty() 247 | } 248 | 249 | option.isSet = true 250 | option.preventDefault = true 251 | option.clearReferenceBeforeSet = false 252 | 253 | if len(option.Choices) != 0 { 254 | found := false 255 | 256 | for _, choice := range option.Choices { 257 | if choice == *value { 258 | found = true 259 | break 260 | } 261 | } 262 | 263 | if !found { 264 | allowed := strings.Join(option.Choices[0:len(option.Choices)-1], ", ") 265 | 266 | if len(option.Choices) > 1 { 267 | allowed += " or " + option.Choices[len(option.Choices)-1] 268 | } 269 | 270 | return newErrorf(ErrInvalidChoice, 271 | "Invalid value `%s' for option `%s'. Allowed values are: %s", 272 | *value, option, allowed) 273 | } 274 | } 275 | 276 | if option.isFunc() { 277 | return option.call(value) 278 | } else if value != nil { 279 | return convert(*value, option.value, option.tag) 280 | } 281 | 282 | return convert("", option.value, option.tag) 283 | } 284 | 285 | func (option *Option) setDefault(value *string) error { 286 | if option.preventDefault { 287 | return nil 288 | } 289 | 290 | if err := option.Set(value); err != nil { 291 | return err 292 | } 293 | 294 | option.isSetDefault = true 295 | option.preventDefault = false 296 | 297 | return nil 298 | } 299 | 300 | func (option *Option) showInHelp() bool { 301 | return !option.Hidden && (option.ShortName != 0 || len(option.LongName) != 0) 302 | } 303 | 304 | func (option *Option) canArgument() bool { 305 | if u := option.isUnmarshaler(); u != nil { 306 | return true 307 | } 308 | 309 | return !option.isBool() 310 | } 311 | 312 | func (option *Option) emptyValue() reflect.Value { 313 | tp := option.value.Type() 314 | 315 | if tp.Kind() == reflect.Map { 316 | return reflect.MakeMap(tp) 317 | } 318 | 319 | return reflect.Zero(tp) 320 | } 321 | 322 | func (option *Option) empty() { 323 | if !option.isFunc() { 324 | option.value.Set(option.emptyValue()) 325 | } 326 | } 327 | 328 | func (option *Option) clearDefault() error { 329 | if option.preventDefault { 330 | return nil 331 | } 332 | 333 | usedDefault := option.Default 334 | 335 | if envKey := option.EnvKeyWithNamespace(); envKey != "" { 336 | if value, ok := os.LookupEnv(envKey); ok { 337 | if option.EnvDefaultDelim != "" { 338 | usedDefault = strings.Split(value, option.EnvDefaultDelim) 339 | } else { 340 | usedDefault = []string{value} 341 | } 342 | } 343 | } 344 | 345 | option.isSetDefault = true 346 | 347 | if len(usedDefault) > 0 { 348 | option.empty() 349 | 350 | for _, d := range usedDefault { 351 | err := option.setDefault(&d) 352 | 353 | if err != nil { 354 | return err 355 | } 356 | } 357 | } else { 358 | tp := option.value.Type() 359 | 360 | switch tp.Kind() { 361 | case reflect.Map: 362 | if option.value.IsNil() { 363 | option.empty() 364 | } 365 | case reflect.Slice: 366 | if option.value.IsNil() { 367 | option.empty() 368 | } 369 | } 370 | } 371 | 372 | return nil 373 | } 374 | 375 | func (option *Option) valueIsDefault() bool { 376 | // Check if the value of the option corresponds to its 377 | // default value 378 | emptyval := option.emptyValue() 379 | 380 | checkvalptr := reflect.New(emptyval.Type()) 381 | checkval := reflect.Indirect(checkvalptr) 382 | 383 | checkval.Set(emptyval) 384 | 385 | if len(option.Default) != 0 { 386 | for _, v := range option.Default { 387 | convert(v, checkval, option.tag) 388 | } 389 | } 390 | 391 | return reflect.DeepEqual(option.value.Interface(), checkval.Interface()) 392 | } 393 | 394 | func (option *Option) isUnmarshaler() Unmarshaler { 395 | v := option.value 396 | 397 | for { 398 | if !v.CanInterface() { 399 | break 400 | } 401 | 402 | i := v.Interface() 403 | 404 | if u, ok := i.(Unmarshaler); ok { 405 | return u 406 | } 407 | 408 | if !v.CanAddr() { 409 | break 410 | } 411 | 412 | v = v.Addr() 413 | } 414 | 415 | return nil 416 | } 417 | 418 | func (option *Option) isValueValidator() ValueValidator { 419 | v := option.value 420 | 421 | for { 422 | if !v.CanInterface() { 423 | break 424 | } 425 | 426 | i := v.Interface() 427 | 428 | if u, ok := i.(ValueValidator); ok { 429 | return u 430 | } 431 | 432 | if !v.CanAddr() { 433 | break 434 | } 435 | 436 | v = v.Addr() 437 | } 438 | 439 | return nil 440 | } 441 | 442 | func (option *Option) isBool() bool { 443 | tp := option.value.Type() 444 | 445 | for { 446 | switch tp.Kind() { 447 | case reflect.Slice, reflect.Ptr: 448 | tp = tp.Elem() 449 | case reflect.Bool: 450 | return true 451 | case reflect.Func: 452 | return tp.NumIn() == 0 453 | default: 454 | return false 455 | } 456 | } 457 | } 458 | 459 | func (option *Option) isSignedNumber() bool { 460 | tp := option.value.Type() 461 | 462 | for { 463 | switch tp.Kind() { 464 | case reflect.Slice, reflect.Ptr: 465 | tp = tp.Elem() 466 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64: 467 | return true 468 | default: 469 | return false 470 | } 471 | } 472 | } 473 | 474 | func (option *Option) isFunc() bool { 475 | return option.value.Type().Kind() == reflect.Func 476 | } 477 | 478 | func (option *Option) call(value *string) error { 479 | var retval []reflect.Value 480 | 481 | if value == nil { 482 | retval = option.value.Call(nil) 483 | } else { 484 | tp := option.value.Type().In(0) 485 | 486 | val := reflect.New(tp) 487 | val = reflect.Indirect(val) 488 | 489 | if err := convert(*value, val, option.tag); err != nil { 490 | return err 491 | } 492 | 493 | retval = option.value.Call([]reflect.Value{val}) 494 | } 495 | 496 | if len(retval) == 1 && retval[0].Type() == reflect.TypeOf((*error)(nil)).Elem() { 497 | if retval[0].Interface() == nil { 498 | return nil 499 | } 500 | 501 | return retval[0].Interface().(error) 502 | } 503 | 504 | return nil 505 | } 506 | 507 | func (option *Option) updateDefaultLiteral() { 508 | defs := option.Default 509 | def := "" 510 | 511 | if len(defs) == 0 && option.canArgument() { 512 | var showdef bool 513 | 514 | switch option.field.Type.Kind() { 515 | case reflect.Func, reflect.Ptr: 516 | showdef = !option.value.IsNil() 517 | case reflect.Slice, reflect.String, reflect.Array: 518 | showdef = option.value.Len() > 0 519 | case reflect.Map: 520 | showdef = !option.value.IsNil() && option.value.Len() > 0 521 | default: 522 | zeroval := reflect.Zero(option.field.Type) 523 | showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) 524 | } 525 | 526 | if showdef { 527 | def, _ = convertToString(option.value, option.tag) 528 | } 529 | } else if len(defs) != 0 { 530 | l := len(defs) - 1 531 | 532 | for i := 0; i < l; i++ { 533 | def += quoteIfNeeded(defs[i]) + ", " 534 | } 535 | 536 | def += quoteIfNeeded(defs[l]) 537 | } 538 | 539 | option.defaultLiteral = def 540 | } 541 | 542 | func (option *Option) shortAndLongName() string { 543 | ret := &bytes.Buffer{} 544 | 545 | if option.ShortName != 0 { 546 | ret.WriteRune(defaultShortOptDelimiter) 547 | ret.WriteRune(option.ShortName) 548 | } 549 | 550 | if len(option.LongName) != 0 { 551 | if option.ShortName != 0 { 552 | ret.WriteRune('/') 553 | } 554 | 555 | ret.WriteString(option.LongName) 556 | } 557 | 558 | return ret.String() 559 | } 560 | 561 | func (option *Option) isValidValue(arg string) error { 562 | if validator := option.isValueValidator(); validator != nil { 563 | return validator.IsValidValue(arg) 564 | } 565 | if argumentIsOption(arg) && !(option.isSignedNumber() && len(arg) > 1 && arg[0] == '-' && arg[1] >= '0' && arg[1] <= '9') { 566 | return fmt.Errorf("expected argument for flag `%s', but got option `%s'", option, arg) 567 | } 568 | return nil 569 | } 570 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestPassDoubleDash(t *testing.T) { 9 | var opts = struct { 10 | Value bool `short:"v"` 11 | }{} 12 | 13 | p := NewParser(&opts, PassDoubleDash) 14 | ret, err := p.ParseArgs([]string{"-v", "--", "-v", "-g"}) 15 | 16 | if err != nil { 17 | t.Fatalf("Unexpected error: %v", err) 18 | return 19 | } 20 | 21 | if !opts.Value { 22 | t.Errorf("Expected Value to be true") 23 | } 24 | 25 | assertStringArray(t, ret, []string{"-v", "-g"}) 26 | } 27 | 28 | func TestPassAfterNonOption(t *testing.T) { 29 | var opts = struct { 30 | Value bool `short:"v"` 31 | }{} 32 | 33 | p := NewParser(&opts, PassAfterNonOption) 34 | ret, err := p.ParseArgs([]string{"-v", "arg", "-v", "-g"}) 35 | 36 | if err != nil { 37 | t.Fatalf("Unexpected error: %v", err) 38 | return 39 | } 40 | 41 | if !opts.Value { 42 | t.Errorf("Expected Value to be true") 43 | } 44 | 45 | assertStringArray(t, ret, []string{"arg", "-v", "-g"}) 46 | } 47 | 48 | func TestPassAfterNonOptionWithPositional(t *testing.T) { 49 | var opts = struct { 50 | Value bool `short:"v"` 51 | 52 | Positional struct { 53 | Rest []string `required:"yes"` 54 | } `positional-args:"yes"` 55 | }{} 56 | 57 | p := NewParser(&opts, PassAfterNonOption) 58 | ret, err := p.ParseArgs([]string{"-v", "arg", "-v", "-g"}) 59 | 60 | if err != nil { 61 | t.Fatalf("Unexpected error: %v", err) 62 | return 63 | } 64 | 65 | if !opts.Value { 66 | t.Errorf("Expected Value to be true") 67 | } 68 | 69 | assertStringArray(t, ret, []string{}) 70 | assertStringArray(t, opts.Positional.Rest, []string{"arg", "-v", "-g"}) 71 | } 72 | 73 | func TestPassAfterNonOptionWithPositionalIntPass(t *testing.T) { 74 | var opts = struct { 75 | Value bool `short:"v"` 76 | 77 | Positional struct { 78 | Rest []int `required:"yes"` 79 | } `positional-args:"yes"` 80 | }{} 81 | 82 | p := NewParser(&opts, PassAfterNonOption) 83 | ret, err := p.ParseArgs([]string{"-v", "1", "2", "3"}) 84 | 85 | if err != nil { 86 | t.Fatalf("Unexpected error: %v", err) 87 | return 88 | } 89 | 90 | if !opts.Value { 91 | t.Errorf("Expected Value to be true") 92 | } 93 | 94 | assertStringArray(t, ret, []string{}) 95 | for i, rest := range opts.Positional.Rest { 96 | if rest != i+1 { 97 | assertErrorf(t, "Expected %v got %v", i+1, rest) 98 | } 99 | } 100 | } 101 | 102 | func TestPassAfterNonOptionWithPositionalIntFail(t *testing.T) { 103 | var opts = struct { 104 | Value bool `short:"v"` 105 | 106 | Positional struct { 107 | Rest []int `required:"yes"` 108 | } `positional-args:"yes"` 109 | }{} 110 | 111 | tests := []struct { 112 | opts []string 113 | errContains string 114 | ret []string 115 | }{ 116 | { 117 | []string{"-v", "notint1", "notint2", "notint3"}, 118 | "notint1", 119 | []string{"notint1", "notint2", "notint3"}, 120 | }, 121 | { 122 | []string{"-v", "1", "notint2", "notint3"}, 123 | "notint2", 124 | []string{"1", "notint2", "notint3"}, 125 | }, 126 | } 127 | 128 | for _, test := range tests { 129 | p := NewParser(&opts, PassAfterNonOption) 130 | ret, err := p.ParseArgs(test.opts) 131 | 132 | if err == nil { 133 | assertErrorf(t, "Expected error") 134 | return 135 | } 136 | 137 | if !strings.Contains(err.Error(), test.errContains) { 138 | assertErrorf(t, "Expected the first illegal argument in the error") 139 | } 140 | 141 | assertStringArray(t, ret, test.ret) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /optstyle_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows || forceposix 2 | // +build !windows forceposix 3 | 4 | package flags 5 | 6 | import ( 7 | "strings" 8 | ) 9 | 10 | const ( 11 | defaultShortOptDelimiter = '-' 12 | defaultLongOptDelimiter = "--" 13 | defaultNameArgDelimiter = '=' 14 | ) 15 | 16 | func argumentStartsOption(arg string) bool { 17 | return len(arg) > 0 && arg[0] == '-' 18 | } 19 | 20 | func argumentIsOption(arg string) bool { 21 | if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' { 22 | return true 23 | } 24 | 25 | if len(arg) > 2 && arg[0] == '-' && arg[1] == '-' && arg[2] != '-' { 26 | return true 27 | } 28 | 29 | return false 30 | } 31 | 32 | // stripOptionPrefix returns the option without the prefix and whether or 33 | // not the option is a long option or not. 34 | func stripOptionPrefix(optname string) (prefix string, name string, islong bool) { 35 | if strings.HasPrefix(optname, "--") { 36 | return "--", optname[2:], true 37 | } else if strings.HasPrefix(optname, "-") { 38 | return "-", optname[1:], false 39 | } 40 | 41 | return "", optname, false 42 | } 43 | 44 | // splitOption attempts to split the passed option into a name and an argument. 45 | // When there is no argument specified, nil will be returned for it. 46 | func splitOption(prefix string, option string, islong bool) (string, string, *string) { 47 | pos := strings.Index(option, "=") 48 | 49 | if (islong && pos >= 0) || (!islong && pos == 1) { 50 | rest := option[pos+1:] 51 | return option[:pos], "=", &rest 52 | } 53 | 54 | return option, "", nil 55 | } 56 | 57 | // addHelpGroup adds a new group that contains default help parameters. 58 | func (c *Command) addHelpGroup(showHelp func() error) *Group { 59 | var help struct { 60 | ShowHelp func() error `short:"h" long:"help" description:"Show this help message"` 61 | } 62 | 63 | help.ShowHelp = showHelp 64 | ret, _ := c.AddGroup("Help Options", "", &help) 65 | ret.isBuiltinHelp = true 66 | 67 | return ret 68 | } 69 | -------------------------------------------------------------------------------- /optstyle_windows.go: -------------------------------------------------------------------------------- 1 | //go:build !forceposix 2 | // +build !forceposix 3 | 4 | package flags 5 | 6 | import ( 7 | "strings" 8 | ) 9 | 10 | // Windows uses a front slash for both short and long options. Also it uses 11 | // a colon for name/argument delimter. 12 | const ( 13 | defaultShortOptDelimiter = '/' 14 | defaultLongOptDelimiter = "/" 15 | defaultNameArgDelimiter = ':' 16 | ) 17 | 18 | func argumentStartsOption(arg string) bool { 19 | return len(arg) > 0 && (arg[0] == '-' || arg[0] == '/') 20 | } 21 | 22 | func argumentIsOption(arg string) bool { 23 | // Windows-style options allow front slash for the option 24 | // delimiter. 25 | if len(arg) > 1 && arg[0] == '/' { 26 | return true 27 | } 28 | 29 | if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' { 30 | return true 31 | } 32 | 33 | if len(arg) > 2 && arg[0] == '-' && arg[1] == '-' && arg[2] != '-' { 34 | return true 35 | } 36 | 37 | return false 38 | } 39 | 40 | // stripOptionPrefix returns the option without the prefix and whether or 41 | // not the option is a long option or not. 42 | func stripOptionPrefix(optname string) (prefix string, name string, islong bool) { 43 | // Determine if the argument is a long option or not. Windows 44 | // typically supports both long and short options with a single 45 | // front slash as the option delimiter, so handle this situation 46 | // nicely. 47 | possplit := 0 48 | 49 | if strings.HasPrefix(optname, "--") { 50 | possplit = 2 51 | islong = true 52 | } else if strings.HasPrefix(optname, "-") { 53 | possplit = 1 54 | islong = false 55 | } else if strings.HasPrefix(optname, "/") { 56 | possplit = 1 57 | islong = len(optname) > 2 58 | } 59 | 60 | return optname[:possplit], optname[possplit:], islong 61 | } 62 | 63 | // splitOption attempts to split the passed option into a name and an argument. 64 | // When there is no argument specified, nil will be returned for it. 65 | func splitOption(prefix string, option string, islong bool) (string, string, *string) { 66 | if len(option) == 0 { 67 | return option, "", nil 68 | } 69 | 70 | // Windows typically uses a colon for the option name and argument 71 | // delimiter while POSIX typically uses an equals. Support both styles, 72 | // but don't allow the two to be mixed. That is to say /foo:bar and 73 | // --foo=bar are acceptable, but /foo=bar and --foo:bar are not. 74 | var pos int 75 | var sp string 76 | 77 | if prefix == "/" { 78 | sp = ":" 79 | pos = strings.Index(option, sp) 80 | } else if len(prefix) > 0 { 81 | sp = "=" 82 | pos = strings.Index(option, sp) 83 | } 84 | 85 | if (islong && pos >= 0) || (!islong && pos == 1) { 86 | rest := option[pos+1:] 87 | return option[:pos], sp, &rest 88 | } 89 | 90 | return option, "", nil 91 | } 92 | 93 | // addHelpGroup adds a new group that contains default help parameters. 94 | func (c *Command) addHelpGroup(showHelp func() error) *Group { 95 | // Windows CLI applications typically use /? for help, so make both 96 | // that available as well as the POSIX style h and help. 97 | var help struct { 98 | ShowHelpWindows func() error `short:"?" description:"Show this help message"` 99 | ShowHelpPosix func() error `short:"h" long:"help" description:"Show this help message"` 100 | } 101 | 102 | help.ShowHelpWindows = showHelp 103 | help.ShowHelpPosix = showHelp 104 | 105 | ret, _ := c.AddGroup("Help Options", "", &help) 106 | ret.isBuiltinHelp = true 107 | 108 | return ret 109 | } 110 | -------------------------------------------------------------------------------- /pointer_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPointerBool(t *testing.T) { 8 | var opts = struct { 9 | Value *bool `short:"v"` 10 | }{} 11 | 12 | ret := assertParseSuccess(t, &opts, "-v") 13 | 14 | assertStringArray(t, ret, []string{}) 15 | 16 | if !*opts.Value { 17 | t.Errorf("Expected Value to be true") 18 | } 19 | } 20 | 21 | func TestPointerString(t *testing.T) { 22 | var opts = struct { 23 | Value *string `short:"v"` 24 | }{} 25 | 26 | ret := assertParseSuccess(t, &opts, "-v", "value") 27 | 28 | assertStringArray(t, ret, []string{}) 29 | assertString(t, *opts.Value, "value") 30 | } 31 | 32 | func TestPointerSlice(t *testing.T) { 33 | var opts = struct { 34 | Value *[]string `short:"v"` 35 | }{} 36 | 37 | ret := assertParseSuccess(t, &opts, "-v", "value1", "-v", "value2") 38 | 39 | assertStringArray(t, ret, []string{}) 40 | assertStringArray(t, *opts.Value, []string{"value1", "value2"}) 41 | } 42 | 43 | func TestPointerMap(t *testing.T) { 44 | var opts = struct { 45 | Value *map[string]int `short:"v"` 46 | }{} 47 | 48 | ret := assertParseSuccess(t, &opts, "-v", "k1:2", "-v", "k2:-5") 49 | 50 | assertStringArray(t, ret, []string{}) 51 | 52 | if v, ok := (*opts.Value)["k1"]; !ok { 53 | t.Errorf("Expected key \"k1\" to exist") 54 | } else if v != 2 { 55 | t.Errorf("Expected \"k1\" to be 2, but got %#v", v) 56 | } 57 | 58 | if v, ok := (*opts.Value)["k2"]; !ok { 59 | t.Errorf("Expected key \"k2\" to exist") 60 | } else if v != -5 { 61 | t.Errorf("Expected \"k2\" to be -5, but got %#v", v) 62 | } 63 | } 64 | 65 | type marshalledString string 66 | 67 | func (m *marshalledString) UnmarshalFlag(value string) error { 68 | *m = marshalledString(value) 69 | return nil 70 | } 71 | 72 | func (m marshalledString) MarshalFlag() (string, error) { 73 | return string(m), nil 74 | } 75 | 76 | func TestPointerStringMarshalled(t *testing.T) { 77 | var opts = struct { 78 | Value *marshalledString `short:"v"` 79 | }{} 80 | 81 | ret := assertParseSuccess(t, &opts, "-v", "value") 82 | 83 | assertStringArray(t, ret, []string{}) 84 | 85 | if opts.Value == nil { 86 | t.Error("Expected value not to be nil") 87 | return 88 | } 89 | 90 | assertString(t, string(*opts.Value), "value") 91 | } 92 | 93 | type marshalledStruct struct { 94 | Value string 95 | } 96 | 97 | func (m *marshalledStruct) UnmarshalFlag(value string) error { 98 | m.Value = value 99 | return nil 100 | } 101 | 102 | func (m marshalledStruct) MarshalFlag() (string, error) { 103 | return m.Value, nil 104 | } 105 | 106 | func TestPointerStructMarshalled(t *testing.T) { 107 | var opts = struct { 108 | Value *marshalledStruct `short:"v"` 109 | }{} 110 | 111 | ret := assertParseSuccess(t, &opts, "-v", "value") 112 | 113 | assertStringArray(t, ret, []string{}) 114 | 115 | if opts.Value == nil { 116 | t.Error("Expected value not to be nil") 117 | return 118 | } 119 | 120 | assertString(t, opts.Value.Value, "value") 121 | } 122 | 123 | type PointerGroup struct { 124 | Value bool `short:"v"` 125 | } 126 | 127 | func TestPointerGroup(t *testing.T) { 128 | var opts = struct { 129 | Group *PointerGroup `group:"Group Options"` 130 | }{} 131 | 132 | ret := assertParseSuccess(t, &opts, "-v") 133 | 134 | assertStringArray(t, ret, []string{}) 135 | 136 | if !opts.Group.Value { 137 | t.Errorf("Expected Group.Value to be true") 138 | } 139 | } 140 | 141 | func TestDoNotChangeNonTaggedFields(t *testing.T) { 142 | var opts struct { 143 | A struct { 144 | Pointer *int 145 | } 146 | B *struct { 147 | Pointer *int 148 | } 149 | } 150 | 151 | ret := assertParseSuccess(t, &opts) 152 | 153 | assertStringArray(t, ret, []string{}) 154 | 155 | if opts.A.Pointer != nil { 156 | t.Error("Expected A.Pointer to be nil") 157 | } 158 | if opts.B != nil { 159 | t.Error("Expected B to be nil") 160 | } 161 | if opts.B != nil && opts.B.Pointer != nil { 162 | t.Error("Expected B.Pointer to be nil") 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /short_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestShort(t *testing.T) { 9 | var opts = struct { 10 | Value bool `short:"v"` 11 | }{} 12 | 13 | ret := assertParseSuccess(t, &opts, "-v") 14 | 15 | assertStringArray(t, ret, []string{}) 16 | 17 | if !opts.Value { 18 | t.Errorf("Expected Value to be true") 19 | } 20 | } 21 | 22 | func TestShortTooLong(t *testing.T) { 23 | var opts = struct { 24 | Value bool `short:"vv"` 25 | }{} 26 | 27 | assertParseFail(t, ErrShortNameTooLong, "short names can only be 1 character long, not `vv'", &opts) 28 | } 29 | 30 | func TestShortRequired(t *testing.T) { 31 | var opts = struct { 32 | Value bool `short:"v" required:"true"` 33 | }{} 34 | 35 | assertParseFail(t, ErrRequired, fmt.Sprintf("the required flag `%cv' was not specified", defaultShortOptDelimiter), &opts) 36 | } 37 | 38 | func TestShortRequiredFalsy1(t *testing.T) { 39 | var opts = struct { 40 | Value bool `short:"v" required:"false"` 41 | }{} 42 | 43 | assertParseSuccess(t, &opts) 44 | } 45 | 46 | func TestShortRequiredFalsy2(t *testing.T) { 47 | var opts = struct { 48 | Value bool `short:"v" required:"no"` 49 | }{} 50 | 51 | assertParseSuccess(t, &opts) 52 | } 53 | 54 | func TestShortMultiConcat(t *testing.T) { 55 | var opts = struct { 56 | V bool `short:"v"` 57 | O bool `short:"o"` 58 | F bool `short:"f"` 59 | }{} 60 | 61 | ret := assertParseSuccess(t, &opts, "-vo", "-f") 62 | 63 | assertStringArray(t, ret, []string{}) 64 | 65 | if !opts.V { 66 | t.Errorf("Expected V to be true") 67 | } 68 | 69 | if !opts.O { 70 | t.Errorf("Expected O to be true") 71 | } 72 | 73 | if !opts.F { 74 | t.Errorf("Expected F to be true") 75 | } 76 | } 77 | 78 | func TestShortMultiRequiredConcat(t *testing.T) { 79 | var opts = struct { 80 | V bool `short:"v" required:"true"` 81 | O bool `short:"o" required:"true"` 82 | F bool `short:"f" required:"true"` 83 | }{} 84 | 85 | ret := assertParseSuccess(t, &opts, "-vo", "-f") 86 | 87 | assertStringArray(t, ret, []string{}) 88 | 89 | if !opts.V { 90 | t.Errorf("Expected V to be true") 91 | } 92 | 93 | if !opts.O { 94 | t.Errorf("Expected O to be true") 95 | } 96 | 97 | if !opts.F { 98 | t.Errorf("Expected F to be true") 99 | } 100 | } 101 | 102 | func TestShortMultiSlice(t *testing.T) { 103 | var opts = struct { 104 | Values []bool `short:"v"` 105 | }{} 106 | 107 | ret := assertParseSuccess(t, &opts, "-v", "-v") 108 | 109 | assertStringArray(t, ret, []string{}) 110 | assertBoolArray(t, opts.Values, []bool{true, true}) 111 | } 112 | 113 | func TestShortMultiSliceConcat(t *testing.T) { 114 | var opts = struct { 115 | Values []bool `short:"v"` 116 | }{} 117 | 118 | ret := assertParseSuccess(t, &opts, "-vvv") 119 | 120 | assertStringArray(t, ret, []string{}) 121 | assertBoolArray(t, opts.Values, []bool{true, true, true}) 122 | } 123 | 124 | func TestShortWithEqualArg(t *testing.T) { 125 | var opts = struct { 126 | Value string `short:"v"` 127 | }{} 128 | 129 | ret := assertParseSuccess(t, &opts, "-v=value") 130 | 131 | assertStringArray(t, ret, []string{}) 132 | assertString(t, opts.Value, "value") 133 | } 134 | 135 | func TestShortWithArg(t *testing.T) { 136 | var opts = struct { 137 | Value string `short:"v"` 138 | }{} 139 | 140 | ret := assertParseSuccess(t, &opts, "-vvalue") 141 | 142 | assertStringArray(t, ret, []string{}) 143 | assertString(t, opts.Value, "value") 144 | } 145 | 146 | func TestShortArg(t *testing.T) { 147 | var opts = struct { 148 | Value string `short:"v"` 149 | }{} 150 | 151 | ret := assertParseSuccess(t, &opts, "-v", "value") 152 | 153 | assertStringArray(t, ret, []string{}) 154 | assertString(t, opts.Value, "value") 155 | } 156 | 157 | func TestShortMultiWithEqualArg(t *testing.T) { 158 | var opts = struct { 159 | F []bool `short:"f"` 160 | Value string `short:"v"` 161 | }{} 162 | 163 | assertParseFail(t, ErrExpectedArgument, fmt.Sprintf("expected argument for flag `%cv'", defaultShortOptDelimiter), &opts, "-ffv=value") 164 | } 165 | 166 | func TestShortMultiArg(t *testing.T) { 167 | var opts = struct { 168 | F []bool `short:"f"` 169 | Value string `short:"v"` 170 | }{} 171 | 172 | ret := assertParseSuccess(t, &opts, "-ffv", "value") 173 | 174 | assertStringArray(t, ret, []string{}) 175 | assertBoolArray(t, opts.F, []bool{true, true}) 176 | assertString(t, opts.Value, "value") 177 | } 178 | 179 | func TestShortMultiArgConcatFail(t *testing.T) { 180 | var opts = struct { 181 | F []bool `short:"f"` 182 | Value string `short:"v"` 183 | }{} 184 | 185 | assertParseFail(t, ErrExpectedArgument, fmt.Sprintf("expected argument for flag `%cv'", defaultShortOptDelimiter), &opts, "-ffvvalue") 186 | } 187 | 188 | func TestShortMultiArgConcat(t *testing.T) { 189 | var opts = struct { 190 | F []bool `short:"f"` 191 | Value string `short:"v"` 192 | }{} 193 | 194 | ret := assertParseSuccess(t, &opts, "-vff") 195 | 196 | assertStringArray(t, ret, []string{}) 197 | assertString(t, opts.Value, "ff") 198 | } 199 | 200 | func TestShortOptional(t *testing.T) { 201 | var opts = struct { 202 | F []bool `short:"f"` 203 | Value string `short:"v" optional:"yes" optional-value:"value"` 204 | }{} 205 | 206 | ret := assertParseSuccess(t, &opts, "-fv", "f") 207 | 208 | assertStringArray(t, ret, []string{"f"}) 209 | assertString(t, opts.Value, "value") 210 | } 211 | 212 | func TestShortOptionalFalsy1(t *testing.T) { 213 | var opts = struct { 214 | F []bool `short:"f"` 215 | Value string `short:"v" optional:"false" optional-value:"value"` 216 | }{} 217 | 218 | ret := assertParseSuccess(t, &opts, "-fv", "f") 219 | 220 | assertStringArray(t, ret, []string{}) 221 | assertString(t, opts.Value, "f") 222 | } 223 | 224 | func TestShortOptionalFalsy2(t *testing.T) { 225 | var opts = struct { 226 | F []bool `short:"f"` 227 | Value string `short:"v" optional:"no" optional-value:"value"` 228 | }{} 229 | 230 | ret := assertParseSuccess(t, &opts, "-fv", "f") 231 | 232 | assertStringArray(t, ret, []string{}) 233 | assertString(t, opts.Value, "f") 234 | } 235 | -------------------------------------------------------------------------------- /termsize.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !plan9 && !appengine && !wasm && !aix 2 | // +build !windows,!plan9,!appengine,!wasm,!aix 3 | 4 | package flags 5 | 6 | import ( 7 | "flag" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func getTerminalColumns() int { 13 | if flag.Lookup("test.v") != nil { 14 | return defaultTermSize 15 | } 16 | 17 | ws, err := unix.IoctlGetWinsize(0, unix.TIOCGWINSZ) 18 | if err != nil { 19 | return defaultTermSize 20 | } 21 | return int(ws.Col) 22 | } 23 | -------------------------------------------------------------------------------- /termsize_defaults.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | const defaultTermSize = 80 4 | -------------------------------------------------------------------------------- /termsize_nosysioctl.go: -------------------------------------------------------------------------------- 1 | //go:build plan9 || appengine || wasm || aix 2 | // +build plan9 appengine wasm aix 3 | 4 | package flags 5 | 6 | func getTerminalColumns() int { 7 | return defaultTermSize 8 | } 9 | -------------------------------------------------------------------------------- /termsize_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package flags 5 | 6 | import ( 7 | "flag" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | type ( 13 | SHORT int16 14 | WORD uint16 15 | 16 | SMALL_RECT struct { 17 | Left SHORT 18 | Top SHORT 19 | Right SHORT 20 | Bottom SHORT 21 | } 22 | 23 | COORD struct { 24 | X SHORT 25 | Y SHORT 26 | } 27 | 28 | CONSOLE_SCREEN_BUFFER_INFO struct { 29 | Size COORD 30 | CursorPosition COORD 31 | Attributes WORD 32 | Window SMALL_RECT 33 | MaximumWindowSize COORD 34 | } 35 | ) 36 | 37 | var kernel32DLL = syscall.NewLazyDLL("kernel32.dll") 38 | var getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") 39 | 40 | func getError(r1, r2 uintptr, lastErr error) error { 41 | // If the function fails, the return value is zero. 42 | if r1 == 0 { 43 | if lastErr != nil { 44 | return lastErr 45 | } 46 | return syscall.EINVAL 47 | } 48 | return nil 49 | } 50 | 51 | func getStdHandle(stdhandle int) (uintptr, error) { 52 | handle, err := syscall.GetStdHandle(stdhandle) 53 | if err != nil { 54 | return 0, err 55 | } 56 | return uintptr(handle), nil 57 | } 58 | 59 | // GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer. 60 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx 61 | func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) { 62 | var info CONSOLE_SCREEN_BUFFER_INFO 63 | if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil { 64 | return nil, err 65 | } 66 | return &info, nil 67 | } 68 | 69 | func getTerminalColumns() int { 70 | if flag.Lookup("test.v") != nil { 71 | return defaultTermSize 72 | } 73 | 74 | stdoutHandle, err := getStdHandle(syscall.STD_OUTPUT_HANDLE) 75 | if err != nil { 76 | return defaultTermSize 77 | } 78 | 79 | info, err := GetConsoleScreenBufferInfo(stdoutHandle) 80 | if err != nil { 81 | return defaultTermSize 82 | } 83 | 84 | if info.MaximumWindowSize.X > 0 { 85 | return int(info.MaximumWindowSize.X) 86 | } 87 | 88 | return defaultTermSize 89 | } 90 | -------------------------------------------------------------------------------- /unknown_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUnknownFlags(t *testing.T) { 8 | var opts = struct { 9 | Verbose []bool `short:"v" long:"verbose" description:"Verbose output"` 10 | }{} 11 | 12 | args := []string{ 13 | "-f", 14 | } 15 | 16 | p := NewParser(&opts, 0) 17 | args, err := p.ParseArgs(args) 18 | 19 | if err == nil { 20 | t.Fatal("Expected error for unknown argument") 21 | } 22 | } 23 | 24 | func TestIgnoreUnknownFlags(t *testing.T) { 25 | var opts = struct { 26 | Verbose []bool `short:"v" long:"verbose" description:"Verbose output"` 27 | }{} 28 | 29 | args := []string{ 30 | "hello", 31 | "world", 32 | "-v", 33 | "--foo=bar", 34 | "--verbose", 35 | "-f", 36 | } 37 | 38 | p := NewParser(&opts, IgnoreUnknown) 39 | args, err := p.ParseArgs(args) 40 | 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | exargs := []string{ 46 | "hello", 47 | "world", 48 | "--foo=bar", 49 | "-f", 50 | } 51 | 52 | issame := (len(args) == len(exargs)) 53 | 54 | if issame { 55 | for i := 0; i < len(args); i++ { 56 | if args[i] != exargs[i] { 57 | issame = false 58 | break 59 | } 60 | } 61 | } 62 | 63 | if !issame { 64 | t.Fatalf("Expected %v but got %v", exargs, args) 65 | } 66 | } 67 | --------------------------------------------------------------------------------