├── .travis.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 ├── 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 ├── tag_test.go ├── termsize.go ├── termsize_nosysioctl.go ├── termsize_windows.go └── unknown_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | os: 4 | - linux 5 | - osx 6 | 7 | go: 8 | - 1.x 9 | - 1.9.x 10 | - 1.10.x 11 | 12 | install: 13 | # go-flags 14 | - go get -d -v ./... 15 | - go build -v ./... 16 | 17 | # linting 18 | - go get -v golang.org/x/lint/golint 19 | 20 | # code coverage 21 | - go get golang.org/x/tools/cmd/cover 22 | - go get github.com/onsi/ginkgo/ginkgo 23 | - go get github.com/modocache/gover 24 | - if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then go get github.com/mattn/goveralls; fi 25 | 26 | script: 27 | # go-flags 28 | - $(exit $(gofmt -l . | wc -l)) 29 | - go test -v ./... 30 | 31 | # linting 32 | - go tool vet -all=true -v=true . || true 33 | - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/golint ./... 34 | 35 | # code coverage 36 | - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/ginkgo -r -cover 37 | - $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/gover 38 | - if [ "$TRAVIS_SECURE_ENV_VARS" = "true" ]; then $(go env GOPATH | awk 'BEGIN{FS=":"} {print $1}')/bin/goveralls -coverprofile=gover.coverprofile -service=travis-ci -repotoken $COVERALLS_TOKEN; fi 39 | 40 | env: 41 | # coveralls.io 42 | secure: "RCYbiB4P0RjQRIoUx/vG/AjP3mmYCbzOmr86DCww1Z88yNcy3hYr3Cq8rpPtYU5v0g7wTpu4adaKIcqRE9xknYGbqj3YWZiCoBP1/n4Z+9sHW3Dsd9D/GRGeHUus0laJUGARjWoCTvoEtOgTdGQDoX7mH+pUUY0FBltNYUdOiiU=" 43 | -------------------------------------------------------------------------------- /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) [![Build Status](https://travis-ci.org/jessevdk/go-flags.svg?branch=master)](https://travis-ci.org/jessevdk/go-flags) [![Coverage Status](https://img.shields.io/coveralls/jessevdk/go-flags.svg)](https://coveralls.io/r/jessevdk/go-flags?branch=master) 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 | 83 | // Callback which will invoke callto: to call a number. 84 | // Note that this works just on OS X (and probably only with 85 | // Skype) but it shows the idea. 86 | opts.Call = func(num string) { 87 | cmd := exec.Command("open", "callto:"+num) 88 | cmd.Start() 89 | cmd.Process.Release() 90 | } 91 | 92 | // Make some fake arguments to parse. 93 | args := []string{ 94 | "-vv", 95 | "--offset=5", 96 | "-n", "Me", 97 | "--animal", "dog", // anything other than "cat" or "dog" will raise an error 98 | "-p", "3", 99 | "-s", "hello", 100 | "-s", "world", 101 | "--ptrslice", "hello", 102 | "--ptrslice", "world", 103 | "--intmap", "a:1", 104 | "--intmap", "b:5", 105 | "arg1", 106 | "arg2", 107 | "arg3", 108 | } 109 | 110 | // Parse flags from `args'. Note that here we use flags.ParseArgs for 111 | // the sake of making a working example. Normally, you would simply use 112 | // flags.Parse(&opts) which uses os.Args 113 | args, err := flags.ParseArgs(&opts, args) 114 | 115 | if err != nil { 116 | panic(err) 117 | } 118 | 119 | fmt.Printf("Verbosity: %v\n", opts.Verbose) 120 | fmt.Printf("Offset: %d\n", opts.Offset) 121 | fmt.Printf("Name: %s\n", opts.Name) 122 | fmt.Printf("Animal: %s\n", opts.Animal) 123 | fmt.Printf("Ptr: %d\n", *opts.Ptr) 124 | fmt.Printf("StringSlice: %v\n", opts.StringSlice) 125 | fmt.Printf("PtrSlice: [%v %v]\n", *opts.PtrSlice[0], *opts.PtrSlice[1]) 126 | fmt.Printf("IntMap: [a:%v b:%v]\n", opts.IntMap["a"], opts.IntMap["b"]) 127 | fmt.Printf("Remaining args: %s\n", strings.Join(args, " ")) 128 | 129 | // Output: Verbosity: [true true] 130 | // Offset: 5 131 | // Name: Me 132 | // Ptr: 3 133 | // StringSlice: [hello world] 134 | // PtrSlice: [hello world] 135 | // IntMap: [a:1 b:5] 136 | // Remaining args: arg1 arg2 arg3 137 | ``` 138 | 139 | More information can be found in the godocs: 140 | -------------------------------------------------------------------------------- /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 | "io" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "runtime" 11 | "testing" 12 | ) 13 | 14 | func assertCallerInfo() (string, int) { 15 | ptr := make([]uintptr, 15) 16 | n := runtime.Callers(1, ptr) 17 | 18 | if n == 0 { 19 | return "", 0 20 | } 21 | 22 | mef := runtime.FuncForPC(ptr[0]) 23 | mefile, meline := mef.FileLine(ptr[0]) 24 | 25 | for i := 2; i < n; i++ { 26 | f := runtime.FuncForPC(ptr[i]) 27 | file, line := f.FileLine(ptr[i]) 28 | 29 | if file != mefile { 30 | return file, line 31 | } 32 | } 33 | 34 | return mefile, meline 35 | } 36 | 37 | func assertErrorf(t *testing.T, format string, args ...interface{}) { 38 | msg := fmt.Sprintf(format, args...) 39 | 40 | file, line := assertCallerInfo() 41 | 42 | t.Errorf("%s:%d: %s", path.Base(file), line, msg) 43 | } 44 | 45 | func assertFatalf(t *testing.T, format string, args ...interface{}) { 46 | msg := fmt.Sprintf(format, args...) 47 | 48 | file, line := assertCallerInfo() 49 | 50 | t.Fatalf("%s:%d: %s", path.Base(file), line, msg) 51 | } 52 | 53 | func assertString(t *testing.T, a string, b string) { 54 | if a != b { 55 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 56 | } 57 | } 58 | 59 | func assertStringArray(t *testing.T, a []string, b []string) { 60 | if len(a) != len(b) { 61 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 62 | return 63 | } 64 | 65 | for i, v := range a { 66 | if b[i] != v { 67 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 68 | return 69 | } 70 | } 71 | } 72 | 73 | func assertBoolArray(t *testing.T, a []bool, b []bool) { 74 | if len(a) != len(b) { 75 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 76 | return 77 | } 78 | 79 | for i, v := range a { 80 | if b[i] != v { 81 | assertErrorf(t, "Expected %#v, but got %#v", b, a) 82 | return 83 | } 84 | } 85 | } 86 | 87 | func assertParserSuccess(t *testing.T, data interface{}, args ...string) (*Parser, []string) { 88 | parser := NewParser(data, Default&^PrintErrors) 89 | ret, err := parser.ParseArgs(args) 90 | 91 | if err != nil { 92 | t.Fatalf("Unexpected parse error: %s", err) 93 | return nil, nil 94 | } 95 | 96 | return parser, ret 97 | } 98 | 99 | func assertParseSuccess(t *testing.T, data interface{}, args ...string) []string { 100 | _, ret := assertParserSuccess(t, data, args...) 101 | return ret 102 | } 103 | 104 | func assertError(t *testing.T, err error, typ ErrorType, msg string) { 105 | if err == nil { 106 | assertFatalf(t, "Expected error: \"%s\", but no error occurred", msg) 107 | return 108 | } 109 | 110 | if e, ok := err.(*Error); !ok { 111 | assertFatalf(t, "Expected Error type, but got %#v", err) 112 | } else { 113 | if e.Type != typ { 114 | assertErrorf(t, "Expected error type {%s}, but got {%s}", typ, e.Type) 115 | } 116 | 117 | if e.Message != msg { 118 | assertErrorf(t, "Expected error message %#v, but got %#v", msg, e.Message) 119 | } 120 | } 121 | } 122 | 123 | func assertParseFail(t *testing.T, typ ErrorType, msg string, data interface{}, args ...string) []string { 124 | parser := NewParser(data, Default&^PrintErrors) 125 | ret, err := parser.ParseArgs(args) 126 | 127 | assertError(t, err, typ, msg) 128 | return ret 129 | } 130 | 131 | func diff(a, b string) (string, error) { 132 | atmp, err := ioutil.TempFile("", "help-diff") 133 | 134 | if err != nil { 135 | return "", err 136 | } 137 | 138 | btmp, err := ioutil.TempFile("", "help-diff") 139 | 140 | if err != nil { 141 | return "", err 142 | } 143 | 144 | if _, err := io.WriteString(atmp, a); err != nil { 145 | return "", err 146 | } 147 | 148 | if _, err := io.WriteString(btmp, b); err != nil { 149 | return "", err 150 | } 151 | 152 | ret, err := exec.Command("diff", "-u", "-d", "--label", "got", atmp.Name(), "--label", "expected", btmp.Name()).Output() 153 | 154 | os.Remove(atmp.Name()) 155 | os.Remove(btmp.Name()) 156 | 157 | if err.Error() == "exit status 1" { 158 | return string(ret), nil 159 | } 160 | 161 | return string(ret), err 162 | } 163 | 164 | func assertDiff(t *testing.T, actual, expected, msg string) { 165 | if actual == expected { 166 | return 167 | } 168 | 169 | ret, err := diff(actual, expected) 170 | 171 | if err != nil { 172 | assertErrorf(t, "Unexpected diff error: %s", err) 173 | assertErrorf(t, "Unexpected %s, expected:\n\n%s\n\nbut got\n\n%s", msg, expected, actual) 174 | } else { 175 | assertErrorf(t, "Unexpected %s:\n\n%s", msg, ret) 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /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 | commands []*Command 34 | hasBuiltinHelpGroup bool 35 | args []*Arg 36 | } 37 | 38 | // Commander is an interface which can be implemented by any command added in 39 | // the options. When implemented, the Execute method will be called for the last 40 | // specified (sub)command providing the remaining command line arguments. 41 | type Commander interface { 42 | // Execute will be called for the last active (sub)command. The 43 | // args argument contains the remaining command line arguments. The 44 | // error that Execute returns will be eventually passed out of the 45 | // Parse method of the Parser. 46 | Execute(args []string) error 47 | } 48 | 49 | // Usage is an interface which can be implemented to show a custom usage string 50 | // in the help message shown for a command. 51 | type Usage interface { 52 | // Usage is called for commands to allow customized printing of command 53 | // usage in the generated help message. 54 | Usage() string 55 | } 56 | 57 | type lookup struct { 58 | shortNames map[string]*Option 59 | longNames map[string]*Option 60 | 61 | commands map[string]*Command 62 | } 63 | 64 | // AddCommand adds a new command to the parser with the given name and data. The 65 | // data needs to be a pointer to a struct from which the fields indicate which 66 | // options are in the command. The provided data can implement the Command and 67 | // Usage interfaces. 68 | func (c *Command) AddCommand(command string, shortDescription string, longDescription string, data interface{}) (*Command, error) { 69 | cmd := newCommand(command, shortDescription, longDescription, data) 70 | 71 | cmd.parent = c 72 | 73 | if err := cmd.scan(); err != nil { 74 | return nil, err 75 | } 76 | 77 | c.commands = append(c.commands, cmd) 78 | return cmd, nil 79 | } 80 | 81 | // AddGroup adds a new group to the command with the given name and data. The 82 | // data needs to be a pointer to a struct from which the fields indicate which 83 | // options are in the group. 84 | func (c *Command) AddGroup(shortDescription string, longDescription string, data interface{}) (*Group, error) { 85 | group := newGroup(shortDescription, longDescription, data) 86 | 87 | group.parent = c 88 | 89 | if err := group.scanType(c.scanSubcommandHandler(group)); err != nil { 90 | return nil, err 91 | } 92 | 93 | c.groups = append(c.groups, group) 94 | return group, nil 95 | } 96 | 97 | // Commands returns a list of subcommands of this command. 98 | func (c *Command) Commands() []*Command { 99 | return c.commands 100 | } 101 | 102 | // Find locates the subcommand with the given name and returns it. If no such 103 | // command can be found Find will return nil. 104 | func (c *Command) Find(name string) *Command { 105 | for _, cc := range c.commands { 106 | if cc.match(name) { 107 | return cc 108 | } 109 | } 110 | 111 | return nil 112 | } 113 | 114 | // FindOptionByLongName finds an option that is part of the command, or any of 115 | // its parent commands, by matching its long name (including the option 116 | // namespace). 117 | func (c *Command) FindOptionByLongName(longName string) (option *Option) { 118 | for option == nil && c != nil { 119 | option = c.Group.FindOptionByLongName(longName) 120 | 121 | c, _ = c.parent.(*Command) 122 | } 123 | 124 | return option 125 | } 126 | 127 | // FindOptionByShortName finds an option that is part of the command, or any of 128 | // its parent commands, by matching its long name (including the option 129 | // namespace). 130 | func (c *Command) FindOptionByShortName(shortName rune) (option *Option) { 131 | for option == nil && c != nil { 132 | option = c.Group.FindOptionByShortName(shortName) 133 | 134 | c, _ = c.parent.(*Command) 135 | } 136 | 137 | return option 138 | } 139 | 140 | // Args returns a list of positional arguments associated with this command. 141 | func (c *Command) Args() []*Arg { 142 | ret := make([]*Arg, len(c.args)) 143 | copy(ret, c.args) 144 | 145 | return ret 146 | } 147 | 148 | func newCommand(name string, shortDescription string, longDescription string, data interface{}) *Command { 149 | return &Command{ 150 | Group: newGroup(shortDescription, longDescription, data), 151 | Name: name, 152 | } 153 | } 154 | 155 | func (c *Command) scanSubcommandHandler(parentg *Group) scanHandler { 156 | f := func(realval reflect.Value, sfield *reflect.StructField) (bool, error) { 157 | mtag := newMultiTag(string(sfield.Tag)) 158 | 159 | if err := mtag.Parse(); err != nil { 160 | return true, err 161 | } 162 | 163 | positional := mtag.Get("positional-args") 164 | 165 | if len(positional) != 0 { 166 | stype := realval.Type() 167 | 168 | for i := 0; i < stype.NumField(); i++ { 169 | field := stype.Field(i) 170 | 171 | m := newMultiTag((string(field.Tag))) 172 | 173 | if err := m.Parse(); err != nil { 174 | return true, err 175 | } 176 | 177 | name := m.Get("positional-arg-name") 178 | 179 | if len(name) == 0 { 180 | name = field.Name 181 | } 182 | 183 | required := -1 184 | requiredMaximum := -1 185 | 186 | sreq := m.Get("required") 187 | 188 | if sreq != "" { 189 | required = 1 190 | 191 | rng := strings.SplitN(sreq, "-", 2) 192 | 193 | if len(rng) > 1 { 194 | if preq, err := strconv.ParseInt(rng[0], 10, 32); err == nil { 195 | required = int(preq) 196 | } 197 | 198 | if preq, err := strconv.ParseInt(rng[1], 10, 32); err == nil { 199 | requiredMaximum = int(preq) 200 | } 201 | } else { 202 | if preq, err := strconv.ParseInt(sreq, 10, 32); err == nil { 203 | required = int(preq) 204 | } 205 | } 206 | } 207 | 208 | arg := &Arg{ 209 | Name: name, 210 | Description: m.Get("description"), 211 | Required: required, 212 | RequiredMaximum: requiredMaximum, 213 | 214 | value: realval.Field(i), 215 | tag: m, 216 | } 217 | 218 | c.args = append(c.args, arg) 219 | 220 | if len(mtag.Get("required")) != 0 { 221 | c.ArgsRequired = true 222 | } 223 | } 224 | 225 | return true, nil 226 | } 227 | 228 | subcommand := mtag.Get("command") 229 | 230 | if len(subcommand) != 0 { 231 | var ptrval reflect.Value 232 | 233 | if realval.Kind() == reflect.Ptr { 234 | ptrval = realval 235 | 236 | if ptrval.IsNil() { 237 | ptrval.Set(reflect.New(ptrval.Type().Elem())) 238 | } 239 | } else { 240 | ptrval = realval.Addr() 241 | } 242 | 243 | shortDescription := mtag.Get("description") 244 | longDescription := mtag.Get("long-description") 245 | subcommandsOptional := mtag.Get("subcommands-optional") 246 | aliases := mtag.GetMany("alias") 247 | 248 | subc, err := c.AddCommand(subcommand, shortDescription, longDescription, ptrval.Interface()) 249 | 250 | if err != nil { 251 | return true, err 252 | } 253 | 254 | subc.Hidden = mtag.Get("hidden") != "" 255 | 256 | if len(subcommandsOptional) > 0 { 257 | subc.SubcommandsOptional = true 258 | } 259 | 260 | if len(aliases) > 0 { 261 | subc.Aliases = aliases 262 | } 263 | 264 | return true, nil 265 | } 266 | 267 | return parentg.scanSubGroupHandler(realval, sfield) 268 | } 269 | 270 | return f 271 | } 272 | 273 | func (c *Command) scan() error { 274 | return c.scanType(c.scanSubcommandHandler(c.Group)) 275 | } 276 | 277 | func (c *Command) eachOption(f func(*Command, *Group, *Option)) { 278 | c.eachCommand(func(c *Command) { 279 | c.eachGroup(func(g *Group) { 280 | for _, option := range g.options { 281 | f(c, g, option) 282 | } 283 | }) 284 | }, true) 285 | } 286 | 287 | func (c *Command) eachCommand(f func(*Command), recurse bool) { 288 | f(c) 289 | 290 | for _, cc := range c.commands { 291 | if recurse { 292 | cc.eachCommand(f, true) 293 | } else { 294 | f(cc) 295 | } 296 | } 297 | } 298 | 299 | func (c *Command) eachActiveGroup(f func(cc *Command, g *Group)) { 300 | c.eachGroup(func(g *Group) { 301 | f(c, g) 302 | }) 303 | 304 | if c.Active != nil { 305 | c.Active.eachActiveGroup(f) 306 | } 307 | } 308 | 309 | func (c *Command) addHelpGroups(showHelp func() error) { 310 | if !c.hasBuiltinHelpGroup { 311 | c.addHelpGroup(showHelp) 312 | c.hasBuiltinHelpGroup = true 313 | } 314 | 315 | for _, cc := range c.commands { 316 | cc.addHelpGroups(showHelp) 317 | } 318 | } 319 | 320 | func (c *Command) makeLookup() lookup { 321 | ret := lookup{ 322 | shortNames: make(map[string]*Option), 323 | longNames: make(map[string]*Option), 324 | commands: make(map[string]*Command), 325 | } 326 | 327 | parent := c.parent 328 | 329 | var parents []*Command 330 | 331 | for parent != nil { 332 | if cmd, ok := parent.(*Command); ok { 333 | parents = append(parents, cmd) 334 | parent = cmd.parent 335 | } else { 336 | parent = nil 337 | } 338 | } 339 | 340 | for i := len(parents) - 1; i >= 0; i-- { 341 | parents[i].fillLookup(&ret, true) 342 | } 343 | 344 | c.fillLookup(&ret, false) 345 | return ret 346 | } 347 | 348 | func (c *Command) fillLookup(ret *lookup, onlyOptions bool) { 349 | c.eachGroup(func(g *Group) { 350 | for _, option := range g.options { 351 | if option.ShortName != 0 { 352 | ret.shortNames[string(option.ShortName)] = option 353 | } 354 | 355 | if len(option.LongName) > 0 { 356 | ret.longNames[option.LongNameWithNamespace()] = option 357 | } 358 | } 359 | }) 360 | 361 | if onlyOptions { 362 | return 363 | } 364 | 365 | for _, subcommand := range c.commands { 366 | ret.commands[subcommand.Name] = subcommand 367 | 368 | for _, a := range subcommand.Aliases { 369 | ret.commands[a] = subcommand 370 | } 371 | } 372 | } 373 | 374 | func (c *Command) groupByName(name string) *Group { 375 | if grp := c.Group.groupByName(name); grp != nil { 376 | return grp 377 | } 378 | 379 | for _, subc := range c.commands { 380 | prefix := subc.Name + "." 381 | 382 | if strings.HasPrefix(name, prefix) { 383 | if grp := subc.groupByName(name[len(prefix):]); grp != nil { 384 | return grp 385 | } 386 | } else if name == subc.Name { 387 | return subc.Group 388 | } 389 | } 390 | 391 | return nil 392 | } 393 | 394 | type commandList []*Command 395 | 396 | func (c commandList) Less(i, j int) bool { 397 | return c[i].Name < c[j].Name 398 | } 399 | 400 | func (c commandList) Len() int { 401 | return len(c) 402 | } 403 | 404 | func (c commandList) Swap(i, j int) { 405 | c[i], c[j] = c[j], c[i] 406 | } 407 | 408 | func (c *Command) sortedVisibleCommands() []*Command { 409 | ret := commandList(c.visibleCommands()) 410 | sort.Sort(ret) 411 | 412 | return []*Command(ret) 413 | } 414 | 415 | func (c *Command) visibleCommands() []*Command { 416 | ret := make([]*Command, 0, len(c.commands)) 417 | 418 | for _, cmd := range c.commands { 419 | if !cmd.Hidden { 420 | ret = append(ret, cmd) 421 | } 422 | } 423 | 424 | return ret 425 | } 426 | 427 | func (c *Command) match(name string) bool { 428 | if c.Name == name { 429 | return true 430 | } 431 | 432 | for _, v := range c.Aliases { 433 | if v == name { 434 | return true 435 | } 436 | } 437 | 438 | return false 439 | } 440 | 441 | func (c *Command) hasHelpOptions() bool { 442 | ret := false 443 | 444 | c.eachGroup(func(g *Group) { 445 | if g.isBuiltinHelp { 446 | return 447 | } 448 | 449 | for _, opt := range g.options { 450 | if opt.showInHelp() { 451 | ret = true 452 | } 453 | } 454 | }) 455 | 456 | return ret 457 | } 458 | 459 | func (c *Command) fillParseState(s *parseState) { 460 | s.positional = make([]*Arg, len(c.args)) 461 | copy(s.positional, c.args) 462 | 463 | s.lookup = c.makeLookup() 464 | s.command = c 465 | } 466 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | Completion{ 86 | Item: prefix + match, 87 | }, 88 | } 89 | } 90 | 91 | var results []Completion 92 | repeats := map[string]bool{} 93 | 94 | for name, opt := range s.lookup.longNames { 95 | if strings.HasPrefix(name, match) && !opt.Hidden { 96 | results = append(results, Completion{ 97 | Item: defaultLongOptDelimiter + name, 98 | Description: opt.Description, 99 | }) 100 | 101 | if short { 102 | repeats[string(opt.ShortName)] = true 103 | } 104 | } 105 | } 106 | 107 | if short { 108 | for name, opt := range s.lookup.shortNames { 109 | if _, exist := repeats[name]; !exist && strings.HasPrefix(name, match) && !opt.Hidden { 110 | results = append(results, Completion{ 111 | Item: string(defaultShortOptDelimiter) + name, 112 | Description: opt.Description, 113 | }) 114 | } 115 | } 116 | } 117 | 118 | return results 119 | } 120 | 121 | func (c *completion) completeNamesForLongPrefix(s *parseState, prefix string, match string) []Completion { 122 | return c.completeOptionNames(s, prefix, match, false) 123 | } 124 | 125 | func (c *completion) completeNamesForShortPrefix(s *parseState, prefix string, match string) []Completion { 126 | return c.completeOptionNames(s, prefix, match, true) 127 | } 128 | 129 | func (c *completion) completeCommands(s *parseState, match string) []Completion { 130 | n := make([]Completion, 0, len(s.command.commands)) 131 | 132 | for _, cmd := range s.command.commands { 133 | if cmd.data != c && !cmd.Hidden && strings.HasPrefix(cmd.Name, match) { 134 | n = append(n, Completion{ 135 | Item: cmd.Name, 136 | Description: cmd.ShortDescription, 137 | }) 138 | } 139 | } 140 | 141 | return n 142 | } 143 | 144 | func (c *completion) completeValue(value reflect.Value, prefix string, match string) []Completion { 145 | if value.Kind() == reflect.Slice { 146 | value = reflect.New(value.Type().Elem()) 147 | } 148 | i := value.Interface() 149 | 150 | var ret []Completion 151 | 152 | if cmp, ok := i.(Completer); ok { 153 | ret = cmp.Complete(match) 154 | } else if value.CanAddr() { 155 | if cmp, ok = value.Addr().Interface().(Completer); ok { 156 | ret = cmp.Complete(match) 157 | } 158 | } 159 | 160 | for i, v := range ret { 161 | ret[i].Item = prefix + v.Item 162 | } 163 | 164 | return ret 165 | } 166 | 167 | func (c *completion) complete(args []string) []Completion { 168 | if len(args) == 0 { 169 | args = []string{""} 170 | } 171 | 172 | s := &parseState{ 173 | args: args, 174 | } 175 | 176 | c.parser.fillParseState(s) 177 | 178 | var opt *Option 179 | 180 | for len(s.args) > 1 { 181 | arg := s.pop() 182 | 183 | if (c.parser.Options&PassDoubleDash) != None && arg == "--" { 184 | opt = nil 185 | c.skipPositional(s, len(s.args)-1) 186 | 187 | break 188 | } 189 | 190 | if argumentIsOption(arg) { 191 | prefix, optname, islong := stripOptionPrefix(arg) 192 | optname, _, argument := splitOption(prefix, optname, islong) 193 | 194 | if argument == nil { 195 | var o *Option 196 | canarg := true 197 | 198 | if islong { 199 | o = s.lookup.longNames[optname] 200 | } else { 201 | for i, r := range optname { 202 | sname := string(r) 203 | o = s.lookup.shortNames[sname] 204 | 205 | if o == nil { 206 | break 207 | } 208 | 209 | if i == 0 && o.canArgument() && len(optname) != len(sname) { 210 | canarg = false 211 | break 212 | } 213 | } 214 | } 215 | 216 | if o == nil && (c.parser.Options&PassAfterNonOption) != None { 217 | opt = nil 218 | c.skipPositional(s, len(s.args)-1) 219 | 220 | break 221 | } else if o != nil && o.canArgument() && !o.OptionalArgument && canarg { 222 | if len(s.args) > 1 { 223 | s.pop() 224 | } else { 225 | opt = o 226 | } 227 | } 228 | } 229 | } else { 230 | if len(s.positional) > 0 { 231 | if !s.positional[0].isRemaining() { 232 | // Don't advance beyond a remaining positional arg (because 233 | // it consumes all subsequent args). 234 | s.positional = s.positional[1:] 235 | } 236 | } else if cmd, ok := s.lookup.commands[arg]; ok { 237 | cmd.fillParseState(s) 238 | } 239 | 240 | opt = nil 241 | } 242 | } 243 | 244 | lastarg := s.args[len(s.args)-1] 245 | var ret []Completion 246 | 247 | if opt != nil { 248 | // Completion for the argument of 'opt' 249 | ret = c.completeValue(opt.value, "", lastarg) 250 | } else if argumentStartsOption(lastarg) { 251 | // Complete the option 252 | prefix, optname, islong := stripOptionPrefix(lastarg) 253 | optname, split, argument := splitOption(prefix, optname, islong) 254 | 255 | if argument == nil && !islong { 256 | rname, n := utf8.DecodeRuneInString(optname) 257 | sname := string(rname) 258 | 259 | if opt := s.lookup.shortNames[sname]; opt != nil && opt.canArgument() { 260 | ret = c.completeValue(opt.value, prefix+sname, optname[n:]) 261 | } else { 262 | ret = c.completeNamesForShortPrefix(s, prefix, optname) 263 | } 264 | } else if argument != nil { 265 | if islong { 266 | opt = s.lookup.longNames[optname] 267 | } else { 268 | opt = s.lookup.shortNames[optname] 269 | } 270 | 271 | if opt != nil { 272 | ret = c.completeValue(opt.value, prefix+optname+split, *argument) 273 | } 274 | } else if islong { 275 | ret = c.completeNamesForLongPrefix(s, prefix, optname) 276 | } else { 277 | ret = c.completeNamesForShortPrefix(s, prefix, optname) 278 | } 279 | } else if len(s.positional) > 0 { 280 | // Complete for positional argument 281 | ret = c.completeValue(s.positional[0].value, "", lastarg) 282 | } else if len(s.command.commands) > 0 { 283 | // Complete for command 284 | ret = c.completeCommands(s, lastarg) 285 | } 286 | 287 | sort.Sort(completions(ret)) 288 | return ret 289 | } 290 | 291 | func (c *completion) print(items []Completion, showDescriptions bool) { 292 | if showDescriptions && len(items) > 1 { 293 | maxl := 0 294 | 295 | for _, v := range items { 296 | if len(v.Item) > maxl { 297 | maxl = len(v.Item) 298 | } 299 | } 300 | 301 | for _, v := range items { 302 | fmt.Printf("%s", v.Item) 303 | 304 | if len(v.Description) > 0 { 305 | fmt.Printf("%s # %s", strings.Repeat(" ", maxl-len(v.Item)), v.Description) 306 | } 307 | 308 | fmt.Printf("\n") 309 | } 310 | } else { 311 | for _, v := range items { 312 | fmt.Println(v.Item) 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /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 | "strings" 12 | "testing" 13 | ) 14 | 15 | type TestComplete struct { 16 | } 17 | 18 | func (t *TestComplete) Complete(match string) []Completion { 19 | options := []string{ 20 | "hello world", 21 | "hello universe", 22 | "hello multiverse", 23 | } 24 | 25 | ret := make([]Completion, 0, len(options)) 26 | 27 | for _, o := range options { 28 | if strings.HasPrefix(o, match) { 29 | ret = append(ret, Completion{ 30 | Item: o, 31 | }) 32 | } 33 | } 34 | 35 | return ret 36 | } 37 | 38 | var completionTestOptions struct { 39 | Verbose bool `short:"v" long:"verbose" description:"Verbose messages"` 40 | Debug bool `short:"d" long:"debug" description:"Enable debug"` 41 | Info bool `short:"i" description:"Display info"` 42 | Version bool `long:"version" description:"Show version"` 43 | Required bool `long:"required" required:"true" description:"This is required"` 44 | Hidden bool `long:"hidden" hidden:"true" description:"This is hidden"` 45 | 46 | AddCommand struct { 47 | Positional struct { 48 | Filename Filename 49 | } `positional-args:"yes"` 50 | } `command:"add" description:"add an item"` 51 | 52 | AddMultiCommand struct { 53 | Positional struct { 54 | Filename []Filename 55 | } `positional-args:"yes"` 56 | Extra []Filename `short:"f"` 57 | } `command:"add-multi" description:"add multiple items"` 58 | 59 | AddMultiCommandFlag struct { 60 | Files []Filename `short:"f"` 61 | } `command:"add-multi-flag" description:"add multiple items via flags"` 62 | 63 | RemoveCommand struct { 64 | Other bool `short:"o"` 65 | File Filename `short:"f" long:"filename"` 66 | } `command:"rm" description:"remove an item"` 67 | 68 | RenameCommand struct { 69 | Completed TestComplete `short:"c" long:"completed"` 70 | } `command:"rename" description:"rename an item"` 71 | 72 | HiddenCommand struct { 73 | } `command:"hidden" description:"hidden command" hidden:"true"` 74 | } 75 | 76 | type completionTest struct { 77 | Args []string 78 | Completed []string 79 | ShowDescriptions bool 80 | } 81 | 82 | var completionTests []completionTest 83 | 84 | func init() { 85 | _, sourcefile, _, _ := runtime.Caller(0) 86 | completionTestSourcedir := filepath.Join(filepath.SplitList(path.Dir(sourcefile))...) 87 | 88 | completionTestFilename := []string{filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion_test.go")} 89 | 90 | completionTestSubdir := []string{ 91 | filepath.Join(completionTestSourcedir, "examples/add.go"), 92 | filepath.Join(completionTestSourcedir, "examples/bash-completion"), 93 | filepath.Join(completionTestSourcedir, "examples/main.go"), 94 | filepath.Join(completionTestSourcedir, "examples/rm.go"), 95 | } 96 | 97 | completionTests = []completionTest{ 98 | { 99 | // Short names 100 | []string{"-"}, 101 | []string{"--debug", "--required", "--verbose", "--version", "-i"}, 102 | false, 103 | }, 104 | 105 | { 106 | // Short names full 107 | []string{"-i"}, 108 | []string{"-i"}, 109 | false, 110 | }, 111 | 112 | { 113 | // Short names concatenated 114 | []string{"-dv"}, 115 | []string{"-dv"}, 116 | false, 117 | }, 118 | 119 | { 120 | // Long names 121 | []string{"--"}, 122 | []string{"--debug", "--required", "--verbose", "--version"}, 123 | false, 124 | }, 125 | 126 | { 127 | // Long names with descriptions 128 | []string{"--"}, 129 | []string{ 130 | "--debug # Enable debug", 131 | "--required # This is required", 132 | "--verbose # Verbose messages", 133 | "--version # Show version", 134 | }, 135 | true, 136 | }, 137 | 138 | { 139 | // Long names partial 140 | []string{"--ver"}, 141 | []string{"--verbose", "--version"}, 142 | false, 143 | }, 144 | 145 | { 146 | // Commands 147 | []string{""}, 148 | []string{"add", "add-multi", "add-multi-flag", "rename", "rm"}, 149 | false, 150 | }, 151 | 152 | { 153 | // Commands with descriptions 154 | []string{""}, 155 | []string{ 156 | "add # add an item", 157 | "add-multi # add multiple items", 158 | "add-multi-flag # add multiple items via flags", 159 | "rename # rename an item", 160 | "rm # remove an item", 161 | }, 162 | true, 163 | }, 164 | 165 | { 166 | // Commands partial 167 | []string{"r"}, 168 | []string{"rename", "rm"}, 169 | false, 170 | }, 171 | 172 | { 173 | // Positional filename 174 | []string{"add", filepath.Join(completionTestSourcedir, "completion")}, 175 | completionTestFilename, 176 | false, 177 | }, 178 | 179 | { 180 | // Multiple positional filename (1 arg) 181 | []string{"add-multi", filepath.Join(completionTestSourcedir, "completion")}, 182 | completionTestFilename, 183 | false, 184 | }, 185 | { 186 | // Multiple positional filename (2 args) 187 | []string{"add-multi", filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion")}, 188 | completionTestFilename, 189 | false, 190 | }, 191 | { 192 | // Multiple positional filename (3 args) 193 | []string{"add-multi", filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion.go"), filepath.Join(completionTestSourcedir, "completion")}, 194 | completionTestFilename, 195 | false, 196 | }, 197 | 198 | { 199 | // Flag filename 200 | []string{"rm", "-f", path.Join(completionTestSourcedir, "completion")}, 201 | completionTestFilename, 202 | false, 203 | }, 204 | 205 | { 206 | // Flag short concat last filename 207 | []string{"rm", "-of", path.Join(completionTestSourcedir, "completion")}, 208 | completionTestFilename, 209 | false, 210 | }, 211 | 212 | { 213 | // Flag concat filename 214 | []string{"rm", "-f" + path.Join(completionTestSourcedir, "completion")}, 215 | []string{"-f" + completionTestFilename[0], "-f" + completionTestFilename[1]}, 216 | false, 217 | }, 218 | 219 | { 220 | // Flag equal concat filename 221 | []string{"rm", "-f=" + path.Join(completionTestSourcedir, "completion")}, 222 | []string{"-f=" + completionTestFilename[0], "-f=" + completionTestFilename[1]}, 223 | false, 224 | }, 225 | 226 | { 227 | // Flag concat long filename 228 | []string{"rm", "--filename=" + path.Join(completionTestSourcedir, "completion")}, 229 | []string{"--filename=" + completionTestFilename[0], "--filename=" + completionTestFilename[1]}, 230 | false, 231 | }, 232 | 233 | { 234 | // Flag long filename 235 | []string{"rm", "--filename", path.Join(completionTestSourcedir, "completion")}, 236 | completionTestFilename, 237 | false, 238 | }, 239 | 240 | { 241 | // To subdir 242 | []string{"rm", "--filename", path.Join(completionTestSourcedir, "examples/bash-")}, 243 | []string{path.Join(completionTestSourcedir, "examples/bash-completion/")}, 244 | false, 245 | }, 246 | 247 | { 248 | // Subdirectory 249 | []string{"rm", "--filename", path.Join(completionTestSourcedir, "examples") + "/"}, 250 | completionTestSubdir, 251 | false, 252 | }, 253 | 254 | { 255 | // Custom completed 256 | []string{"rename", "-c", "hello un"}, 257 | []string{"hello universe"}, 258 | false, 259 | }, 260 | { 261 | // Multiple flag filename 262 | []string{"add-multi-flag", "-f", filepath.Join(completionTestSourcedir, "completion")}, 263 | completionTestFilename, 264 | false, 265 | }, 266 | } 267 | } 268 | 269 | func TestCompletion(t *testing.T) { 270 | p := NewParser(&completionTestOptions, Default) 271 | c := &completion{parser: p} 272 | 273 | for _, test := range completionTests { 274 | if test.ShowDescriptions { 275 | continue 276 | } 277 | 278 | ret := c.complete(test.Args) 279 | items := make([]string, len(ret)) 280 | 281 | for i, v := range ret { 282 | items[i] = v.Item 283 | } 284 | 285 | if !reflect.DeepEqual(items, test.Completed) { 286 | t.Errorf("Args: %#v, %#v\n Expected: %#v\n Got: %#v", test.Args, test.ShowDescriptions, test.Completed, items) 287 | } 288 | } 289 | } 290 | 291 | func TestParserCompletion(t *testing.T) { 292 | for _, test := range completionTests { 293 | if test.ShowDescriptions { 294 | os.Setenv("GO_FLAGS_COMPLETION", "verbose") 295 | } else { 296 | os.Setenv("GO_FLAGS_COMPLETION", "1") 297 | } 298 | 299 | tmp := os.Stdout 300 | 301 | r, w, _ := os.Pipe() 302 | os.Stdout = w 303 | 304 | out := make(chan string) 305 | 306 | go func() { 307 | var buf bytes.Buffer 308 | 309 | io.Copy(&buf, r) 310 | 311 | out <- buf.String() 312 | }() 313 | 314 | p := NewParser(&completionTestOptions, None) 315 | 316 | p.CompletionHandler = func(items []Completion) { 317 | comp := &completion{parser: p} 318 | comp.print(items, test.ShowDescriptions) 319 | } 320 | 321 | _, err := p.ParseArgs(test.Args) 322 | 323 | w.Close() 324 | 325 | os.Stdout = tmp 326 | 327 | if err != nil { 328 | t.Fatalf("Unexpected error: %s", err) 329 | } 330 | 331 | got := strings.Split(strings.Trim(<-out, "\n"), "\n") 332 | 333 | if !reflect.DeepEqual(got, test.Completed) { 334 | t.Errorf("Expected: %#v\nGot: %#v", test.Completed, got) 335 | } 336 | } 337 | 338 | os.Setenv("GO_FLAGS_COMPLETION", "") 339 | } 340 | -------------------------------------------------------------------------------- /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.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 | tp := val.Type() 72 | 73 | // Support for time.Duration 74 | if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() { 75 | stringer := val.Interface().(fmt.Stringer) 76 | return stringer.String(), nil 77 | } 78 | 79 | switch tp.Kind() { 80 | case reflect.String: 81 | return val.String(), nil 82 | case reflect.Bool: 83 | if val.Bool() { 84 | return "true", nil 85 | } 86 | 87 | return "false", nil 88 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 89 | base, err := getBase(options, 10) 90 | 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | return strconv.FormatInt(val.Int(), base), nil 96 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 97 | base, err := getBase(options, 10) 98 | 99 | if err != nil { 100 | return "", err 101 | } 102 | 103 | return strconv.FormatUint(val.Uint(), base), nil 104 | case reflect.Float32, reflect.Float64: 105 | return strconv.FormatFloat(val.Float(), 'g', -1, tp.Bits()), nil 106 | case reflect.Slice: 107 | if val.Len() == 0 { 108 | return "", nil 109 | } 110 | 111 | ret := "[" 112 | 113 | for i := 0; i < val.Len(); i++ { 114 | if i != 0 { 115 | ret += ", " 116 | } 117 | 118 | item, err := convertToString(val.Index(i), options) 119 | 120 | if err != nil { 121 | return "", err 122 | } 123 | 124 | ret += item 125 | } 126 | 127 | return ret + "]", nil 128 | case reflect.Map: 129 | ret := "{" 130 | 131 | for i, key := range val.MapKeys() { 132 | if i != 0 { 133 | ret += ", " 134 | } 135 | 136 | keyitem, err := convertToString(key, options) 137 | 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | item, err := convertToString(val.MapIndex(key), options) 143 | 144 | if err != nil { 145 | return "", err 146 | } 147 | 148 | ret += keyitem + ":" + item 149 | } 150 | 151 | return ret + "}", nil 152 | case reflect.Ptr: 153 | return convertToString(reflect.Indirect(val), options) 154 | case reflect.Interface: 155 | if !val.IsNil() { 156 | return convertToString(val.Elem(), options) 157 | } 158 | } 159 | 160 | return "", nil 161 | } 162 | 163 | func convertUnmarshal(val string, retval reflect.Value) (bool, error) { 164 | if retval.Type().NumMethod() > 0 && retval.CanInterface() { 165 | if unmarshaler, ok := retval.Interface().(Unmarshaler); ok { 166 | if retval.IsNil() { 167 | retval.Set(reflect.New(retval.Type().Elem())) 168 | 169 | // Re-assign from the new value 170 | unmarshaler = retval.Interface().(Unmarshaler) 171 | } 172 | 173 | return true, unmarshaler.UnmarshalFlag(val) 174 | } 175 | } 176 | 177 | if retval.Type().Kind() != reflect.Ptr && retval.CanAddr() { 178 | return convertUnmarshal(val, retval.Addr()) 179 | } 180 | 181 | if retval.Type().Kind() == reflect.Interface && !retval.IsNil() { 182 | return convertUnmarshal(val, retval.Elem()) 183 | } 184 | 185 | return false, nil 186 | } 187 | 188 | func convert(val string, retval reflect.Value, options multiTag) error { 189 | if ok, err := convertUnmarshal(val, retval); ok { 190 | return err 191 | } 192 | 193 | tp := retval.Type() 194 | 195 | // Support for time.Duration 196 | if tp == reflect.TypeOf((*time.Duration)(nil)).Elem() { 197 | parsed, err := time.ParseDuration(val) 198 | 199 | if err != nil { 200 | return err 201 | } 202 | 203 | retval.SetInt(int64(parsed)) 204 | return nil 205 | } 206 | 207 | switch tp.Kind() { 208 | case reflect.String: 209 | retval.SetString(val) 210 | case reflect.Bool: 211 | if val == "" { 212 | retval.SetBool(true) 213 | } else { 214 | b, err := strconv.ParseBool(val) 215 | 216 | if err != nil { 217 | return err 218 | } 219 | 220 | retval.SetBool(b) 221 | } 222 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 223 | base, err := getBase(options, 10) 224 | 225 | if err != nil { 226 | return err 227 | } 228 | 229 | parsed, err := strconv.ParseInt(val, base, tp.Bits()) 230 | 231 | if err != nil { 232 | return err 233 | } 234 | 235 | retval.SetInt(parsed) 236 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 237 | base, err := getBase(options, 10) 238 | 239 | if err != nil { 240 | return err 241 | } 242 | 243 | parsed, err := strconv.ParseUint(val, base, tp.Bits()) 244 | 245 | if err != nil { 246 | return err 247 | } 248 | 249 | retval.SetUint(parsed) 250 | case reflect.Float32, reflect.Float64: 251 | parsed, err := strconv.ParseFloat(val, tp.Bits()) 252 | 253 | if err != nil { 254 | return err 255 | } 256 | 257 | retval.SetFloat(parsed) 258 | case reflect.Slice: 259 | elemtp := tp.Elem() 260 | 261 | elemvalptr := reflect.New(elemtp) 262 | elemval := reflect.Indirect(elemvalptr) 263 | 264 | if err := convert(val, elemval, options); err != nil { 265 | return err 266 | } 267 | 268 | retval.Set(reflect.Append(retval, elemval)) 269 | case reflect.Map: 270 | parts := strings.SplitN(val, ":", 2) 271 | 272 | key := parts[0] 273 | var value string 274 | 275 | if len(parts) == 2 { 276 | value = parts[1] 277 | } 278 | 279 | keytp := tp.Key() 280 | keyval := reflect.New(keytp) 281 | 282 | if err := convert(key, keyval, options); err != nil { 283 | return err 284 | } 285 | 286 | valuetp := tp.Elem() 287 | valueval := reflect.New(valuetp) 288 | 289 | if err := convert(value, valueval, options); err != nil { 290 | return err 291 | } 292 | 293 | if retval.IsNil() { 294 | retval.Set(reflect.MakeMap(tp)) 295 | } 296 | 297 | retval.SetMapIndex(reflect.Indirect(keyval), reflect.Indirect(valueval)) 298 | case reflect.Ptr: 299 | if retval.IsNil() { 300 | retval.Set(reflect.New(retval.Type().Elem())) 301 | } 302 | 303 | return convert(val, reflect.Indirect(retval), options) 304 | case reflect.Interface: 305 | if !retval.IsNil() { 306 | return convert(val, retval.Elem(), options) 307 | } 308 | } 309 | 310 | return nil 311 | } 312 | 313 | func isPrint(s string) bool { 314 | for _, c := range s { 315 | if !strconv.IsPrint(c) { 316 | return false 317 | } 318 | } 319 | 320 | return true 321 | } 322 | 323 | func quoteIfNeeded(s string) string { 324 | if !isPrint(s) { 325 | return strconv.Quote(s) 326 | } 327 | 328 | return s 329 | } 330 | 331 | func quoteIfNeededV(s []string) []string { 332 | ret := make([]string, len(s)) 333 | 334 | for i, v := range s { 335 | ret[i] = quoteIfNeeded(v) 336 | } 337 | 338 | return ret 339 | } 340 | 341 | func quoteV(s []string) []string { 342 | ret := make([]string, len(s)) 343 | 344 | for i, v := range s { 345 | ret[i] = strconv.Quote(v) 346 | } 347 | 348 | return ret 349 | } 350 | 351 | func unquoteIfPossible(s string) (string, error) { 352 | if len(s) == 0 || s[0] != '"' { 353 | return s, nil 354 | } 355 | 356 | return strconv.Unquote(s) 357 | } 358 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 12 | Supported features 13 | 14 | The following features are supported in go-flags: 15 | 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 | Option default values from ENVIRONMENT_VARIABLES, including slice and map values 21 | Multiple option groups each containing a set of options 22 | Generate and print well-formatted help message 23 | Passing remaining command line arguments after -- (optional) 24 | Ignoring unknown command line options (optional) 25 | Supports -I/usr/include -I=/usr/include -I /usr/include option argument specification 26 | Supports multiple short options -aux 27 | Supports all primitive go types (string, int{8..64}, uint{8..64}, float) 28 | Supports same option multiple times (can store in slice or last option counts) 29 | Supports maps 30 | Supports function callbacks 31 | Supports namespaces for (nested) option groups 32 | 33 | Additional features specific to Windows: 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 | 42 | Basic usage 43 | 44 | The flags package uses structs, reflection and struct field tags 45 | to allow users to specify command line options. This results in very simple 46 | and concise specification of your application options. For example: 47 | 48 | type Options struct { 49 | Verbose []bool `short:"v" long:"verbose" description:"Show verbose debug information"` 50 | } 51 | 52 | This specifies one option with a short name -v and a long name --verbose. 53 | When either -v or --verbose is found on the command line, a 'true' value 54 | will be appended to the Verbose field. e.g. when specifying -vvv, the 55 | resulting value of Verbose will be {[true, true, true]}. 56 | 57 | Slice options work exactly the same as primitive type options, except that 58 | whenever the option is encountered, a value is appended to the slice. 59 | 60 | Map options from string to primitive type are also supported. On the command 61 | line, you specify the value for such an option as key:value. For example 62 | 63 | type Options struct { 64 | AuthorInfo string[string] `short:"a"` 65 | } 66 | 67 | Then, the AuthorInfo map can be filled with something like 68 | -a name:Jesse -a "surname:van den Kieboom". 69 | 70 | Finally, for full control over the conversion between command line argument 71 | values and options, user defined types can choose to implement the Marshaler 72 | and Unmarshaler interfaces. 73 | 74 | 75 | Available field tags 76 | 77 | The following is a list of tags for struct fields supported by go-flags: 78 | 79 | short: the short name of the option (single character) 80 | long: the long name of the option 81 | required: if non empty, makes the option required to appear on the command 82 | line. If a required option is not present, the parser will 83 | return ErrRequired (optional) 84 | description: the description of the option (optional) 85 | long-description: the long description of the option. Currently only 86 | displayed in generated man pages (optional) 87 | no-flag: if non-empty, this field is ignored as an option (optional) 88 | 89 | optional: if non-empty, makes the argument of the option optional. When an 90 | argument is optional it can only be specified using 91 | --option=argument (optional) 92 | optional-value: the value of an optional option when the option occurs 93 | without an argument. This tag can be specified multiple 94 | times in the case of maps or slices (optional) 95 | default: the default value of an option. This tag can be specified 96 | multiple times in the case of slices or maps (optional) 97 | default-mask: when specified, this value will be displayed in the help 98 | instead of the actual default value. This is useful 99 | mostly for hiding otherwise sensitive information from 100 | showing up in the help. If default-mask takes the special 101 | value "-", then no default value will be shown at all 102 | (optional) 103 | env: the default value of the option is overridden from the 104 | specified environment variable, if one has been defined. 105 | (optional) 106 | env-delim: the 'env' default value from environment is split into 107 | multiple values with the given delimiter string, use with 108 | slices and maps (optional) 109 | value-name: the name of the argument value (to be shown in the help) 110 | (optional) 111 | choice: limits the values for an option to a set of values. 112 | Repeat this tag once for each allowable value. 113 | e.g. `long:"animal" choice:"cat" choice:"dog"` 114 | hidden: if non-empty, the option is not visible in the help or man page. 115 | 116 | base: a base (radix) used to convert strings to integer values, the 117 | default base is 10 (i.e. decimal) (optional) 118 | 119 | ini-name: the explicit ini option name (optional) 120 | no-ini: if non-empty this field is ignored as an ini option 121 | (optional) 122 | 123 | group: when specified on a struct field, makes the struct 124 | field a separate group with the given name (optional) 125 | namespace: when specified on a group struct field, the namespace 126 | gets prepended to every option's long name and 127 | subgroup's namespace of this group, separated by 128 | the parser's namespace delimiter (optional) 129 | env-namespace: when specified on a group struct field, the env-namespace 130 | gets prepended to every option's env key and 131 | subgroup's env-namespace of this group, separated by 132 | the parser's env-namespace delimiter (optional) 133 | command: when specified on a struct field, makes the struct 134 | field a (sub)command with the given name (optional) 135 | subcommands-optional: when specified on a command struct field, makes 136 | any subcommands of that command optional (optional) 137 | alias: when specified on a command struct field, adds the 138 | specified name as an alias for the command. Can be 139 | be specified multiple times to add more than one 140 | alias (optional) 141 | positional-args: when specified on a field with a struct type, 142 | uses the fields of that struct to parse remaining 143 | positional command line arguments into (in order 144 | of the fields). If a field has a slice type, 145 | then all remaining arguments will be added to it. 146 | Positional arguments are optional by default, 147 | unless the "required" tag is specified together 148 | with the "positional-args" tag. The "required" tag 149 | can also be set on the individual rest argument 150 | fields, to require only the first N positional 151 | arguments. If the "required" tag is set on the 152 | rest arguments slice, then its value determines 153 | the minimum amount of rest arguments that needs to 154 | be provided (e.g. `required:"2"`) (optional) 155 | positional-arg-name: used on a field in a positional argument struct; name 156 | of the positional argument placeholder to be shown in 157 | the help (optional) 158 | 159 | Either the `short:` tag or the `long:` must be specified to make the field eligible as an 160 | option. 161 | 162 | 163 | Option groups 164 | 165 | Option groups are a simple way to semantically separate your options. All 166 | options in a particular group are shown together in the help under the name 167 | of the group. Namespaces can be used to specify option long names more 168 | precisely and emphasize the options affiliation to their group. 169 | 170 | There are currently three ways to specify option groups. 171 | 172 | 1. Use NewNamedParser specifying the various option groups. 173 | 2. Use AddGroup to add a group to an existing parser. 174 | 3. Add a struct field to the top-level options annotated with the 175 | group:"group-name" tag. 176 | 177 | 178 | 179 | Commands 180 | 181 | The flags package also has basic support for commands. Commands are often 182 | used in monolithic applications that support various commands or actions. 183 | Take git for example, all of the add, commit, checkout, etc. are called 184 | commands. Using commands you can easily separate multiple functions of your 185 | application. 186 | 187 | There are currently two ways to specify a command. 188 | 189 | 1. Use AddCommand on an existing parser. 190 | 2. Add a struct field to your options struct annotated with the 191 | command:"command-name" tag. 192 | 193 | The most common, idiomatic way to implement commands is to define a global 194 | parser instance and implement each command in a separate file. These 195 | command files should define a go init function which calls AddCommand on 196 | the global parser. 197 | 198 | When parsing ends and there is an active command and that command implements 199 | the Commander interface, then its Execute method will be run with the 200 | remaining command line arguments. 201 | 202 | Command structs can have options which become valid to parse after the 203 | command has been specified on the command line, in addition to the options 204 | of all the parent commands. I.e. considering a -v flag on the parser and an 205 | add command, the following are equivalent: 206 | 207 | ./app -v add 208 | ./app add -v 209 | 210 | However, if the -v flag is defined on the add command, then the first of 211 | the two examples above would fail since the -v flag is not defined before 212 | the add command. 213 | 214 | 215 | Completion 216 | 217 | go-flags has builtin support to provide bash completion of flags, commands 218 | and argument values. To use completion, the binary which uses go-flags 219 | can be invoked in a special environment to list completion of the current 220 | command line argument. It should be noted that this `executes` your application, 221 | and it is up to the user to make sure there are no negative side effects (for 222 | example from init functions). 223 | 224 | Setting the environment variable `GO_FLAGS_COMPLETION=1` enables completion 225 | by replacing the argument parsing routine with the completion routine which 226 | outputs completions for the passed arguments. The basic invocation to 227 | complete a set of arguments is therefore: 228 | 229 | GO_FLAGS_COMPLETION=1 ./completion-example arg1 arg2 arg3 230 | 231 | where `completion-example` is the binary, `arg1` and `arg2` are 232 | the current arguments, and `arg3` (the last argument) is the argument 233 | to be completed. If the GO_FLAGS_COMPLETION is set to "verbose", then 234 | descriptions of possible completion items will also be shown, if there 235 | are more than 1 completion items. 236 | 237 | To use this with bash completion, a simple file can be written which 238 | calls the binary which supports go-flags completion: 239 | 240 | _completion_example() { 241 | # All arguments except the first one 242 | args=("${COMP_WORDS[@]:1:$COMP_CWORD}") 243 | 244 | # Only split on newlines 245 | local IFS=$'\n' 246 | 247 | # Call completion (note that the first element of COMP_WORDS is 248 | # the executable itself) 249 | COMPREPLY=($(GO_FLAGS_COMPLETION=1 ${COMP_WORDS[0]} "${args[@]}")) 250 | return 0 251 | } 252 | 253 | complete -F _completion_example completion-example 254 | 255 | Completion requires the parser option PassDoubleDash and is therefore enforced if the environment variable GO_FLAGS_COMPLETION is set. 256 | 257 | Customized completion for argument values is supported by implementing 258 | the flags.Completer interface for the argument value type. An example 259 | of a type which does so is the flags.Filename type, an alias of string 260 | allowing simple filename completion. A slice or array argument value 261 | whose element type implements flags.Completer will also be completed. 262 | */ 263 | package flags 264 | -------------------------------------------------------------------------------- /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 !grp.showInHelp() { 76 | return 77 | } 78 | if c != prevcmd { 79 | for _, arg := range c.args { 80 | ret.updateLen(arg.Name, c != p.Command) 81 | } 82 | } 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 | fmt.Fprintf(wr, "[%s]", name) 338 | } else { 339 | fmt.Fprintf(wr, "%s", name) 340 | } 341 | } 342 | 343 | if allcmd.Active == nil && len(allcmd.commands) > 0 { 344 | var co, cc string 345 | 346 | if allcmd.SubcommandsOptional { 347 | co, cc = "[", "]" 348 | } else { 349 | co, cc = "<", ">" 350 | } 351 | 352 | visibleCommands := allcmd.visibleCommands() 353 | 354 | if len(visibleCommands) > 3 { 355 | fmt.Fprintf(wr, " %scommand%s", co, cc) 356 | } else { 357 | subcommands := allcmd.sortedVisibleCommands() 358 | names := make([]string, len(subcommands)) 359 | 360 | for i, subc := range subcommands { 361 | names[i] = subc.Name 362 | } 363 | 364 | fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc) 365 | } 366 | } 367 | 368 | allcmd = allcmd.Active 369 | } 370 | 371 | fmt.Fprintln(wr) 372 | 373 | if len(cmd.LongDescription) != 0 { 374 | fmt.Fprintln(wr) 375 | 376 | t := wrapText(cmd.LongDescription, 377 | aligninfo.terminalColumns, 378 | "") 379 | 380 | fmt.Fprintln(wr, t) 381 | } 382 | } 383 | 384 | c := p.Command 385 | 386 | for c != nil { 387 | printcmd := c != p.Command 388 | 389 | c.eachGroup(func(grp *Group) { 390 | first := true 391 | 392 | // Skip built-in help group for all commands except the top-level 393 | // parser 394 | if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) { 395 | return 396 | } 397 | 398 | for _, info := range grp.options { 399 | if !info.showInHelp() { 400 | continue 401 | } 402 | 403 | if printcmd { 404 | fmt.Fprintf(wr, "\n[%s command options]\n", c.Name) 405 | aligninfo.indent = true 406 | printcmd = false 407 | } 408 | 409 | if first && cmd.Group != grp { 410 | fmt.Fprintln(wr) 411 | 412 | if aligninfo.indent { 413 | wr.WriteString(" ") 414 | } 415 | 416 | fmt.Fprintf(wr, "%s:\n", grp.ShortDescription) 417 | first = false 418 | } 419 | 420 | p.writeHelpOption(wr, info, aligninfo) 421 | } 422 | }) 423 | 424 | var args []*Arg 425 | for _, arg := range c.args { 426 | if arg.Description != "" { 427 | args = append(args, arg) 428 | } 429 | } 430 | 431 | if len(args) > 0 { 432 | if c == p.Command { 433 | fmt.Fprintf(wr, "\nArguments:\n") 434 | } else { 435 | fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name) 436 | } 437 | 438 | descStart := aligninfo.descriptionStart() + paddingBeforeOption 439 | 440 | for _, arg := range args { 441 | argPrefix := strings.Repeat(" ", paddingBeforeOption) 442 | argPrefix += arg.Name 443 | 444 | if len(arg.Description) > 0 { 445 | argPrefix += ":" 446 | wr.WriteString(argPrefix) 447 | 448 | // Space between "arg:" and the description start 449 | descPadding := strings.Repeat(" ", descStart-len(argPrefix)) 450 | // How much space the description gets before wrapping 451 | descWidth := aligninfo.terminalColumns - 1 - descStart 452 | // Whitespace to which we can indent new description lines 453 | descPrefix := strings.Repeat(" ", descStart) 454 | 455 | wr.WriteString(descPadding) 456 | wr.WriteString(wrapText(arg.Description, descWidth, descPrefix)) 457 | } else { 458 | wr.WriteString(argPrefix) 459 | } 460 | 461 | fmt.Fprintln(wr) 462 | } 463 | } 464 | 465 | c = c.Active 466 | } 467 | 468 | scommands := cmd.sortedVisibleCommands() 469 | 470 | if len(scommands) > 0 { 471 | maxnamelen := maxCommandLength(scommands) 472 | 473 | fmt.Fprintln(wr) 474 | fmt.Fprintln(wr, "Available commands:") 475 | 476 | for _, c := range scommands { 477 | fmt.Fprintf(wr, " %s", c.Name) 478 | 479 | if len(c.ShortDescription) > 0 { 480 | pad := strings.Repeat(" ", maxnamelen-len(c.Name)) 481 | fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription) 482 | 483 | if len(c.Aliases) > 0 { 484 | fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", ")) 485 | } 486 | 487 | } 488 | 489 | fmt.Fprintln(wr) 490 | } 491 | } 492 | 493 | wr.Flush() 494 | } 495 | 496 | // WroteHelp is a helper to test the error from ParseArgs() to 497 | // determine if the help message was written. It is safe to 498 | // call without first checking that error is nil. 499 | func WroteHelp(err error) bool { 500 | if err == nil { // No error 501 | return false 502 | } 503 | 504 | flagError, ok := err.(*Error) 505 | if !ok { // Not a go-flag error 506 | return false 507 | } 508 | 509 | if flagError.Type != ErrHelp { // Did not print the help message 510 | return false 511 | } 512 | 513 | return true 514 | } 515 | -------------------------------------------------------------------------------- /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 | var quotesLookup = make(map[*Option]bool) 503 | 504 | for name, section := range ini.Sections { 505 | groups := i.matchingGroups(name) 506 | 507 | if len(groups) == 0 { 508 | if (p.Options & IgnoreUnknown) == None { 509 | return newErrorf(ErrUnknownGroup, "could not find option group `%s'", name) 510 | } 511 | 512 | continue 513 | } 514 | 515 | for _, inival := range section { 516 | var opt *Option 517 | 518 | for _, group := range groups { 519 | opt = group.optionByName(inival.Name, func(o *Option, n string) bool { 520 | return strings.ToLower(o.tag.Get("ini-name")) == strings.ToLower(n) 521 | }) 522 | 523 | if opt != nil && len(opt.tag.Get("no-ini")) != 0 { 524 | opt = nil 525 | } 526 | 527 | if opt != nil { 528 | break 529 | } 530 | } 531 | 532 | if opt == nil { 533 | if (p.Options & IgnoreUnknown) == None { 534 | return &IniError{ 535 | Message: fmt.Sprintf("unknown option: %s", inival.Name), 536 | File: ini.File, 537 | LineNumber: inival.LineNumber, 538 | } 539 | } 540 | 541 | continue 542 | } 543 | 544 | // ini value is ignored if override is set and 545 | // value was previously set from non default 546 | if i.ParseAsDefaults && !opt.isSetDefault { 547 | continue 548 | } 549 | 550 | pval := &inival.Value 551 | 552 | if !opt.canArgument() && len(inival.Value) == 0 { 553 | pval = nil 554 | } else { 555 | if opt.value.Type().Kind() == reflect.Map { 556 | parts := strings.SplitN(inival.Value, ":", 2) 557 | 558 | // only handle unquoting 559 | if len(parts) == 2 && parts[1][0] == '"' { 560 | if v, err := strconv.Unquote(parts[1]); err == nil { 561 | parts[1] = v 562 | 563 | inival.Quoted = true 564 | } else { 565 | return &IniError{ 566 | Message: err.Error(), 567 | File: ini.File, 568 | LineNumber: inival.LineNumber, 569 | } 570 | } 571 | 572 | s := parts[0] + ":" + parts[1] 573 | 574 | pval = &s 575 | } 576 | } 577 | } 578 | 579 | if err := opt.set(pval); err != nil { 580 | return &IniError{ 581 | Message: err.Error(), 582 | File: ini.File, 583 | LineNumber: inival.LineNumber, 584 | } 585 | } 586 | 587 | // either all INI values are quoted or only values who need quoting 588 | if _, ok := quotesLookup[opt]; !inival.Quoted || !ok { 589 | quotesLookup[opt] = inival.Quoted 590 | } 591 | 592 | opt.tag.Set("_read-ini-name", inival.Name) 593 | } 594 | } 595 | 596 | for opt, quoted := range quotesLookup { 597 | opt.iniQuote = quoted 598 | } 599 | 600 | return nil 601 | } 602 | -------------------------------------------------------------------------------- /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 manQuote(s string) string { 14 | return strings.Replace(s, "\\", "\\\\", -1) 15 | } 16 | 17 | func formatForMan(wr io.Writer, s string) { 18 | for { 19 | idx := strings.IndexRune(s, '`') 20 | 21 | if idx < 0 { 22 | fmt.Fprintf(wr, "%s", manQuote(s)) 23 | break 24 | } 25 | 26 | fmt.Fprintf(wr, "%s", manQuote(s[:idx])) 27 | 28 | s = s[idx+1:] 29 | idx = strings.IndexRune(s, '\'') 30 | 31 | if idx < 0 { 32 | fmt.Fprintf(wr, "%s", manQuote(s)) 33 | break 34 | } 35 | 36 | fmt.Fprintf(wr, "\\fB%s\\fP", manQuote(s[:idx])) 37 | s = s[idx+1:] 38 | } 39 | } 40 | 41 | func writeManPageOptions(wr io.Writer, grp *Group) { 42 | grp.eachGroup(func(group *Group) { 43 | if !group.showInHelp() { 44 | return 45 | } 46 | 47 | // If the parent (grp) has any subgroups, display their descriptions as 48 | // subsection headers similar to the output of --help. 49 | if group.ShortDescription != "" && len(grp.groups) > 0 { 50 | fmt.Fprintf(wr, ".SS %s\n", group.ShortDescription) 51 | 52 | if group.LongDescription != "" { 53 | formatForMan(wr, group.LongDescription) 54 | fmt.Fprintln(wr, "") 55 | } 56 | } 57 | 58 | for _, opt := range group.options { 59 | if !opt.showInHelp() { 60 | continue 61 | } 62 | 63 | fmt.Fprintln(wr, ".TP") 64 | fmt.Fprintf(wr, "\\fB") 65 | 66 | if opt.ShortName != 0 { 67 | fmt.Fprintf(wr, "\\fB\\-%c\\fR", opt.ShortName) 68 | } 69 | 70 | if len(opt.LongName) != 0 { 71 | if opt.ShortName != 0 { 72 | fmt.Fprintf(wr, ", ") 73 | } 74 | 75 | fmt.Fprintf(wr, "\\fB\\-\\-%s\\fR", manQuote(opt.LongNameWithNamespace())) 76 | } 77 | 78 | if len(opt.ValueName) != 0 || opt.OptionalArgument { 79 | if opt.OptionalArgument { 80 | fmt.Fprintf(wr, " [\\fI%s=%s\\fR]", manQuote(opt.ValueName), manQuote(strings.Join(quoteV(opt.OptionalValue), ", "))) 81 | } else { 82 | fmt.Fprintf(wr, " \\fI%s\\fR", manQuote(opt.ValueName)) 83 | } 84 | } 85 | 86 | if len(opt.Default) != 0 { 87 | fmt.Fprintf(wr, " ", manQuote(strings.Join(quoteV(opt.Default), ", "))) 88 | } else if len(opt.EnvKeyWithNamespace()) != 0 { 89 | if runtime.GOOS == "windows" { 90 | fmt.Fprintf(wr, " ", manQuote(opt.EnvKeyWithNamespace())) 91 | } else { 92 | fmt.Fprintf(wr, " ", manQuote(opt.EnvKeyWithNamespace())) 93 | } 94 | } 95 | 96 | if opt.Required { 97 | fmt.Fprintf(wr, " (\\fIrequired\\fR)") 98 | } 99 | 100 | fmt.Fprintln(wr, "\\fP") 101 | 102 | if len(opt.Description) != 0 { 103 | formatForMan(wr, opt.Description) 104 | fmt.Fprintln(wr, "") 105 | } 106 | } 107 | }) 108 | } 109 | 110 | func writeManPageSubcommands(wr io.Writer, name string, usagePrefix string, root *Command) { 111 | commands := root.sortedVisibleCommands() 112 | 113 | for _, c := range commands { 114 | var nn string 115 | 116 | if c.Hidden { 117 | continue 118 | } 119 | 120 | if len(name) != 0 { 121 | nn = name + " " + c.Name 122 | } else { 123 | nn = c.Name 124 | } 125 | 126 | writeManPageCommand(wr, nn, usagePrefix, c) 127 | } 128 | } 129 | 130 | func writeManPageCommand(wr io.Writer, name string, usagePrefix string, command *Command) { 131 | fmt.Fprintf(wr, ".SS %s\n", name) 132 | fmt.Fprintln(wr, command.ShortDescription) 133 | 134 | if len(command.LongDescription) > 0 { 135 | fmt.Fprintln(wr, "") 136 | 137 | cmdstart := fmt.Sprintf("The %s command", manQuote(command.Name)) 138 | 139 | if strings.HasPrefix(command.LongDescription, cmdstart) { 140 | fmt.Fprintf(wr, "The \\fI%s\\fP command", manQuote(command.Name)) 141 | 142 | formatForMan(wr, command.LongDescription[len(cmdstart):]) 143 | fmt.Fprintln(wr, "") 144 | } else { 145 | formatForMan(wr, command.LongDescription) 146 | fmt.Fprintln(wr, "") 147 | } 148 | } 149 | 150 | var pre = usagePrefix + " " + command.Name 151 | 152 | var usage string 153 | if us, ok := command.data.(Usage); ok { 154 | usage = us.Usage() 155 | } else if command.hasHelpOptions() { 156 | usage = fmt.Sprintf("[%s-OPTIONS]", command.Name) 157 | } 158 | 159 | var nextPrefix = pre 160 | if len(usage) > 0 { 161 | fmt.Fprintf(wr, "\n\\fBUsage\\fP: %s %s\n.TP\n", manQuote(pre), manQuote(usage)) 162 | nextPrefix = pre + " " + usage 163 | } 164 | 165 | if len(command.Aliases) > 0 { 166 | fmt.Fprintf(wr, "\n\\fBAliases\\fP: %s\n\n", manQuote(strings.Join(command.Aliases, ", "))) 167 | } 168 | 169 | writeManPageOptions(wr, command.Group) 170 | writeManPageSubcommands(wr, name, nextPrefix, command) 171 | } 172 | 173 | // WriteManPage writes a basic man page in groff format to the specified 174 | // writer. 175 | func (p *Parser) WriteManPage(wr io.Writer) { 176 | t := time.Now() 177 | source_date_epoch := os.Getenv("SOURCE_DATE_EPOCH") 178 | if source_date_epoch != "" { 179 | sde, err := strconv.ParseInt(source_date_epoch, 10, 64) 180 | if err != nil { 181 | panic(fmt.Sprintf("Invalid SOURCE_DATE_EPOCH: %s", err)) 182 | } 183 | t = time.Unix(sde, 0) 184 | } 185 | 186 | fmt.Fprintf(wr, ".TH %s 1 \"%s\"\n", manQuote(p.Name), t.Format("2 January 2006")) 187 | fmt.Fprintln(wr, ".SH NAME") 188 | fmt.Fprintf(wr, "%s \\- %s\n", manQuote(p.Name), manQuote(p.ShortDescription)) 189 | fmt.Fprintln(wr, ".SH SYNOPSIS") 190 | 191 | usage := p.Usage 192 | 193 | if len(usage) == 0 { 194 | usage = "[OPTIONS]" 195 | } 196 | 197 | fmt.Fprintf(wr, "\\fB%s\\fP %s\n", manQuote(p.Name), manQuote(usage)) 198 | fmt.Fprintln(wr, ".SH DESCRIPTION") 199 | 200 | formatForMan(wr, p.LongDescription) 201 | fmt.Fprintln(wr, "") 202 | 203 | fmt.Fprintln(wr, ".SH OPTIONS") 204 | 205 | writeManPageOptions(wr, p.Command.Group) 206 | 207 | if len(p.visibleCommands()) > 0 { 208 | fmt.Fprintln(wr, ".SH COMMANDS") 209 | 210 | writeManPageSubcommands(wr, "", p.Name+" "+usage, p.Command) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /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 | 88 | defaultLiteral string 89 | } 90 | 91 | // LongNameWithNamespace returns the option's long name with the group namespaces 92 | // prepended by walking up the option's group tree. Namespaces and the long name 93 | // itself are separated by the parser's namespace delimiter. If the long name is 94 | // empty an empty string is returned. 95 | func (option *Option) LongNameWithNamespace() string { 96 | if len(option.LongName) == 0 { 97 | return "" 98 | } 99 | 100 | // fetch the namespace delimiter from the parser which is always at the 101 | // end of the group hierarchy 102 | namespaceDelimiter := "" 103 | g := option.group 104 | 105 | for { 106 | if p, ok := g.parent.(*Parser); ok { 107 | namespaceDelimiter = p.NamespaceDelimiter 108 | 109 | break 110 | } 111 | 112 | switch i := g.parent.(type) { 113 | case *Command: 114 | g = i.Group 115 | case *Group: 116 | g = i 117 | } 118 | } 119 | 120 | // concatenate long name with namespace 121 | longName := option.LongName 122 | g = option.group 123 | 124 | for g != nil { 125 | if g.Namespace != "" { 126 | longName = g.Namespace + namespaceDelimiter + longName 127 | } 128 | 129 | switch i := g.parent.(type) { 130 | case *Command: 131 | g = i.Group 132 | case *Group: 133 | g = i 134 | case *Parser: 135 | g = nil 136 | } 137 | } 138 | 139 | return longName 140 | } 141 | 142 | // EnvKeyWithNamespace returns the option's env key with the group namespaces 143 | // prepended by walking up the option's group tree. Namespaces and the env key 144 | // itself are separated by the parser's namespace delimiter. If the env key is 145 | // empty an empty string is returned. 146 | func (option *Option) EnvKeyWithNamespace() string { 147 | if len(option.EnvDefaultKey) == 0 { 148 | return "" 149 | } 150 | 151 | // fetch the namespace delimiter from the parser which is always at the 152 | // end of the group hierarchy 153 | namespaceDelimiter := "" 154 | g := option.group 155 | 156 | for { 157 | if p, ok := g.parent.(*Parser); ok { 158 | namespaceDelimiter = p.EnvNamespaceDelimiter 159 | 160 | break 161 | } 162 | 163 | switch i := g.parent.(type) { 164 | case *Command: 165 | g = i.Group 166 | case *Group: 167 | g = i 168 | } 169 | } 170 | 171 | // concatenate long name with namespace 172 | key := option.EnvDefaultKey 173 | g = option.group 174 | 175 | for g != nil { 176 | if g.EnvNamespace != "" { 177 | key = g.EnvNamespace + namespaceDelimiter + key 178 | } 179 | 180 | switch i := g.parent.(type) { 181 | case *Command: 182 | g = i.Group 183 | case *Group: 184 | g = i 185 | case *Parser: 186 | g = nil 187 | } 188 | } 189 | 190 | return key 191 | } 192 | 193 | // String converts an option to a human friendly readable string describing the 194 | // option. 195 | func (option *Option) String() string { 196 | var s string 197 | var short string 198 | 199 | if option.ShortName != 0 { 200 | data := make([]byte, utf8.RuneLen(option.ShortName)) 201 | utf8.EncodeRune(data, option.ShortName) 202 | short = string(data) 203 | 204 | if len(option.LongName) != 0 { 205 | s = fmt.Sprintf("%s%s, %s%s", 206 | string(defaultShortOptDelimiter), short, 207 | defaultLongOptDelimiter, option.LongNameWithNamespace()) 208 | } else { 209 | s = fmt.Sprintf("%s%s", string(defaultShortOptDelimiter), short) 210 | } 211 | } else if len(option.LongName) != 0 { 212 | s = fmt.Sprintf("%s%s", defaultLongOptDelimiter, option.LongNameWithNamespace()) 213 | } 214 | 215 | return s 216 | } 217 | 218 | // Value returns the option value as an interface{}. 219 | func (option *Option) Value() interface{} { 220 | return option.value.Interface() 221 | } 222 | 223 | // Field returns the reflect struct field of the option. 224 | func (option *Option) Field() reflect.StructField { 225 | return option.field 226 | } 227 | 228 | // IsSet returns true if option has been set 229 | func (option *Option) IsSet() bool { 230 | return option.isSet 231 | } 232 | 233 | // IsSetDefault returns true if option has been set via the default option tag 234 | func (option *Option) IsSetDefault() bool { 235 | return option.isSetDefault 236 | } 237 | 238 | // Set the value of an option to the specified value. An error will be returned 239 | // if the specified value could not be converted to the corresponding option 240 | // value type. 241 | func (option *Option) set(value *string) error { 242 | kind := option.value.Type().Kind() 243 | 244 | if (kind == reflect.Map || kind == reflect.Slice) && !option.isSet { 245 | option.empty() 246 | } 247 | 248 | option.isSet = true 249 | option.preventDefault = true 250 | 251 | if len(option.Choices) != 0 { 252 | found := false 253 | 254 | for _, choice := range option.Choices { 255 | if choice == *value { 256 | found = true 257 | break 258 | } 259 | } 260 | 261 | if !found { 262 | allowed := strings.Join(option.Choices[0:len(option.Choices)-1], ", ") 263 | 264 | if len(option.Choices) > 1 { 265 | allowed += " or " + option.Choices[len(option.Choices)-1] 266 | } 267 | 268 | return newErrorf(ErrInvalidChoice, 269 | "Invalid value `%s' for option `%s'. Allowed values are: %s", 270 | *value, option, allowed) 271 | } 272 | } 273 | 274 | if option.isFunc() { 275 | return option.call(value) 276 | } else if value != nil { 277 | return convert(*value, option.value, option.tag) 278 | } 279 | 280 | return convert("", option.value, option.tag) 281 | } 282 | 283 | func (option *Option) showInHelp() bool { 284 | return !option.Hidden && (option.ShortName != 0 || len(option.LongName) != 0) 285 | } 286 | 287 | func (option *Option) canArgument() bool { 288 | if u := option.isUnmarshaler(); u != nil { 289 | return true 290 | } 291 | 292 | return !option.isBool() 293 | } 294 | 295 | func (option *Option) emptyValue() reflect.Value { 296 | tp := option.value.Type() 297 | 298 | if tp.Kind() == reflect.Map { 299 | return reflect.MakeMap(tp) 300 | } 301 | 302 | return reflect.Zero(tp) 303 | } 304 | 305 | func (option *Option) empty() { 306 | if !option.isFunc() { 307 | option.value.Set(option.emptyValue()) 308 | } 309 | } 310 | 311 | func (option *Option) clearDefault() error { 312 | usedDefault := option.Default 313 | 314 | if envKey := option.EnvKeyWithNamespace(); envKey != "" { 315 | if value, ok := os.LookupEnv(envKey); ok { 316 | if option.EnvDefaultDelim != "" { 317 | usedDefault = strings.Split(value, option.EnvDefaultDelim) 318 | } else { 319 | usedDefault = []string{value} 320 | } 321 | } 322 | } 323 | 324 | option.isSetDefault = true 325 | 326 | if len(usedDefault) > 0 { 327 | option.empty() 328 | 329 | for _, d := range usedDefault { 330 | err := option.set(&d) 331 | if err != nil { 332 | return err 333 | } 334 | option.isSetDefault = true 335 | } 336 | } else { 337 | tp := option.value.Type() 338 | 339 | switch tp.Kind() { 340 | case reflect.Map: 341 | if option.value.IsNil() { 342 | option.empty() 343 | } 344 | case reflect.Slice: 345 | if option.value.IsNil() { 346 | option.empty() 347 | } 348 | } 349 | } 350 | 351 | return nil 352 | } 353 | 354 | func (option *Option) valueIsDefault() bool { 355 | // Check if the value of the option corresponds to its 356 | // default value 357 | emptyval := option.emptyValue() 358 | 359 | checkvalptr := reflect.New(emptyval.Type()) 360 | checkval := reflect.Indirect(checkvalptr) 361 | 362 | checkval.Set(emptyval) 363 | 364 | if len(option.Default) != 0 { 365 | for _, v := range option.Default { 366 | convert(v, checkval, option.tag) 367 | } 368 | } 369 | 370 | return reflect.DeepEqual(option.value.Interface(), checkval.Interface()) 371 | } 372 | 373 | func (option *Option) isUnmarshaler() Unmarshaler { 374 | v := option.value 375 | 376 | for { 377 | if !v.CanInterface() { 378 | break 379 | } 380 | 381 | i := v.Interface() 382 | 383 | if u, ok := i.(Unmarshaler); ok { 384 | return u 385 | } 386 | 387 | if !v.CanAddr() { 388 | break 389 | } 390 | 391 | v = v.Addr() 392 | } 393 | 394 | return nil 395 | } 396 | 397 | func (option *Option) isValueValidator() ValueValidator { 398 | v := option.value 399 | 400 | for { 401 | if !v.CanInterface() { 402 | break 403 | } 404 | 405 | i := v.Interface() 406 | 407 | if u, ok := i.(ValueValidator); ok { 408 | return u 409 | } 410 | 411 | if !v.CanAddr() { 412 | break 413 | } 414 | 415 | v = v.Addr() 416 | } 417 | 418 | return nil 419 | } 420 | 421 | func (option *Option) isBool() bool { 422 | tp := option.value.Type() 423 | 424 | for { 425 | switch tp.Kind() { 426 | case reflect.Slice, reflect.Ptr: 427 | tp = tp.Elem() 428 | case reflect.Bool: 429 | return true 430 | case reflect.Func: 431 | return tp.NumIn() == 0 432 | default: 433 | return false 434 | } 435 | } 436 | } 437 | 438 | func (option *Option) isSignedNumber() bool { 439 | tp := option.value.Type() 440 | 441 | for { 442 | switch tp.Kind() { 443 | case reflect.Slice, reflect.Ptr: 444 | tp = tp.Elem() 445 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64: 446 | return true 447 | default: 448 | return false 449 | } 450 | } 451 | } 452 | 453 | func (option *Option) isFunc() bool { 454 | return option.value.Type().Kind() == reflect.Func 455 | } 456 | 457 | func (option *Option) call(value *string) error { 458 | var retval []reflect.Value 459 | 460 | if value == nil { 461 | retval = option.value.Call(nil) 462 | } else { 463 | tp := option.value.Type().In(0) 464 | 465 | val := reflect.New(tp) 466 | val = reflect.Indirect(val) 467 | 468 | if err := convert(*value, val, option.tag); err != nil { 469 | return err 470 | } 471 | 472 | retval = option.value.Call([]reflect.Value{val}) 473 | } 474 | 475 | if len(retval) == 1 && retval[0].Type() == reflect.TypeOf((*error)(nil)).Elem() { 476 | if retval[0].Interface() == nil { 477 | return nil 478 | } 479 | 480 | return retval[0].Interface().(error) 481 | } 482 | 483 | return nil 484 | } 485 | 486 | func (option *Option) updateDefaultLiteral() { 487 | defs := option.Default 488 | def := "" 489 | 490 | if len(defs) == 0 && option.canArgument() { 491 | var showdef bool 492 | 493 | switch option.field.Type.Kind() { 494 | case reflect.Func, reflect.Ptr: 495 | showdef = !option.value.IsNil() 496 | case reflect.Slice, reflect.String, reflect.Array: 497 | showdef = option.value.Len() > 0 498 | case reflect.Map: 499 | showdef = !option.value.IsNil() && option.value.Len() > 0 500 | default: 501 | zeroval := reflect.Zero(option.field.Type) 502 | showdef = !reflect.DeepEqual(zeroval.Interface(), option.value.Interface()) 503 | } 504 | 505 | if showdef { 506 | def, _ = convertToString(option.value, option.tag) 507 | } 508 | } else if len(defs) != 0 { 509 | l := len(defs) - 1 510 | 511 | for i := 0; i < l; i++ { 512 | def += quoteIfNeeded(defs[i]) + ", " 513 | } 514 | 515 | def += quoteIfNeeded(defs[l]) 516 | } 517 | 518 | option.defaultLiteral = def 519 | } 520 | 521 | func (option *Option) shortAndLongName() string { 522 | ret := &bytes.Buffer{} 523 | 524 | if option.ShortName != 0 { 525 | ret.WriteRune(defaultShortOptDelimiter) 526 | ret.WriteRune(option.ShortName) 527 | } 528 | 529 | if len(option.LongName) != 0 { 530 | if option.ShortName != 0 { 531 | ret.WriteRune('/') 532 | } 533 | 534 | ret.WriteString(option.LongName) 535 | } 536 | 537 | return ret.String() 538 | } 539 | 540 | func (option *Option) isValidValue(arg string) error { 541 | if validator := option.isValueValidator(); validator != nil { 542 | return validator.IsValidValue(arg) 543 | } 544 | if argumentIsOption(arg) && !(option.isSignedNumber() && len(arg) > 1 && arg[0] == '-' && arg[1] >= '0' && arg[1] <= '9') { 545 | return fmt.Errorf("expected argument for flag `%s', but got option `%s'", option, arg) 546 | } 547 | return nil 548 | } 549 | -------------------------------------------------------------------------------- /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 | // +build !windows forceposix 2 | 3 | package flags 4 | 5 | import ( 6 | "strings" 7 | ) 8 | 9 | const ( 10 | defaultShortOptDelimiter = '-' 11 | defaultLongOptDelimiter = "--" 12 | defaultNameArgDelimiter = '=' 13 | ) 14 | 15 | func argumentStartsOption(arg string) bool { 16 | return len(arg) > 0 && arg[0] == '-' 17 | } 18 | 19 | func argumentIsOption(arg string) bool { 20 | if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' { 21 | return true 22 | } 23 | 24 | if len(arg) > 2 && arg[0] == '-' && arg[1] == '-' && arg[2] != '-' { 25 | return true 26 | } 27 | 28 | return false 29 | } 30 | 31 | // stripOptionPrefix returns the option without the prefix and whether or 32 | // not the option is a long option or not. 33 | func stripOptionPrefix(optname string) (prefix string, name string, islong bool) { 34 | if strings.HasPrefix(optname, "--") { 35 | return "--", optname[2:], true 36 | } else if strings.HasPrefix(optname, "-") { 37 | return "-", optname[1:], false 38 | } 39 | 40 | return "", optname, false 41 | } 42 | 43 | // splitOption attempts to split the passed option into a name and an argument. 44 | // When there is no argument specified, nil will be returned for it. 45 | func splitOption(prefix string, option string, islong bool) (string, string, *string) { 46 | pos := strings.Index(option, "=") 47 | 48 | if (islong && pos >= 0) || (!islong && pos == 1) { 49 | rest := option[pos+1:] 50 | return option[:pos], "=", &rest 51 | } 52 | 53 | return option, "", nil 54 | } 55 | 56 | // addHelpGroup adds a new group that contains default help parameters. 57 | func (c *Command) addHelpGroup(showHelp func() error) *Group { 58 | var help struct { 59 | ShowHelp func() error `short:"h" long:"help" description:"Show this help message"` 60 | } 61 | 62 | help.ShowHelp = showHelp 63 | ret, _ := c.AddGroup("Help Options", "", &help) 64 | ret.isBuiltinHelp = true 65 | 66 | return ret 67 | } 68 | -------------------------------------------------------------------------------- /optstyle_windows.go: -------------------------------------------------------------------------------- 1 | // +build !forceposix 2 | 3 | package flags 4 | 5 | import ( 6 | "strings" 7 | ) 8 | 9 | // Windows uses a front slash for both short and long options. Also it uses 10 | // a colon for name/argument delimter. 11 | const ( 12 | defaultShortOptDelimiter = '/' 13 | defaultLongOptDelimiter = "/" 14 | defaultNameArgDelimiter = ':' 15 | ) 16 | 17 | func argumentStartsOption(arg string) bool { 18 | return len(arg) > 0 && (arg[0] == '-' || arg[0] == '/') 19 | } 20 | 21 | func argumentIsOption(arg string) bool { 22 | // Windows-style options allow front slash for the option 23 | // delimiter. 24 | if len(arg) > 1 && arg[0] == '/' { 25 | return true 26 | } 27 | 28 | if len(arg) > 1 && arg[0] == '-' && arg[1] != '-' { 29 | return true 30 | } 31 | 32 | if len(arg) > 2 && arg[0] == '-' && arg[1] == '-' && arg[2] != '-' { 33 | return true 34 | } 35 | 36 | return false 37 | } 38 | 39 | // stripOptionPrefix returns the option without the prefix and whether or 40 | // not the option is a long option or not. 41 | func stripOptionPrefix(optname string) (prefix string, name string, islong bool) { 42 | // Determine if the argument is a long option or not. Windows 43 | // typically supports both long and short options with a single 44 | // front slash as the option delimiter, so handle this situation 45 | // nicely. 46 | possplit := 0 47 | 48 | if strings.HasPrefix(optname, "--") { 49 | possplit = 2 50 | islong = true 51 | } else if strings.HasPrefix(optname, "-") { 52 | possplit = 1 53 | islong = false 54 | } else if strings.HasPrefix(optname, "/") { 55 | possplit = 1 56 | islong = len(optname) > 2 57 | } 58 | 59 | return optname[:possplit], optname[possplit:], islong 60 | } 61 | 62 | // splitOption attempts to split the passed option into a name and an argument. 63 | // When there is no argument specified, nil will be returned for it. 64 | func splitOption(prefix string, option string, islong bool) (string, string, *string) { 65 | if len(option) == 0 { 66 | return option, "", nil 67 | } 68 | 69 | // Windows typically uses a colon for the option name and argument 70 | // delimiter while POSIX typically uses an equals. Support both styles, 71 | // but don't allow the two to be mixed. That is to say /foo:bar and 72 | // --foo=bar are acceptable, but /foo=bar and --foo:bar are not. 73 | var pos int 74 | var sp string 75 | 76 | if prefix == "/" { 77 | sp = ":" 78 | pos = strings.Index(option, sp) 79 | } else if len(prefix) > 0 { 80 | sp = "=" 81 | pos = strings.Index(option, sp) 82 | } 83 | 84 | if (islong && pos >= 0) || (!islong && pos == 1) { 85 | rest := option[pos+1:] 86 | return option[:pos], sp, &rest 87 | } 88 | 89 | return option, "", nil 90 | } 91 | 92 | // addHelpGroup adds a new group that contains default help parameters. 93 | func (c *Command) addHelpGroup(showHelp func() error) *Group { 94 | // Windows CLI applications typically use /? for help, so make both 95 | // that available as well as the POSIX style h and help. 96 | var help struct { 97 | ShowHelpWindows func() error `short:"?" description:"Show this help message"` 98 | ShowHelpPosix func() error `short:"h" long:"help" description:"Show this help message"` 99 | } 100 | 101 | help.ShowHelpWindows = showHelp 102 | help.ShowHelpPosix = showHelp 103 | 104 | ret, _ := c.AddGroup("Help Options", "", &help) 105 | ret.isBuiltinHelp = true 106 | 107 | return ret 108 | } 109 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "runtime" 9 | "strconv" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | type defaultOptions struct { 16 | Int int `long:"i"` 17 | IntDefault int `long:"id" default:"1"` 18 | 19 | Float64 float64 `long:"f"` 20 | Float64Default float64 `long:"fd" default:"-3.14"` 21 | 22 | NumericFlag bool `short:"3"` 23 | 24 | String string `long:"str"` 25 | StringDefault string `long:"strd" default:"abc"` 26 | StringNotUnquoted string `long:"strnot" unquote:"false"` 27 | 28 | Time time.Duration `long:"t"` 29 | TimeDefault time.Duration `long:"td" default:"1m"` 30 | 31 | Map map[string]int `long:"m"` 32 | MapDefault map[string]int `long:"md" default:"a:1"` 33 | 34 | Slice []int `long:"s"` 35 | SliceDefault []int `long:"sd" default:"1" default:"2"` 36 | } 37 | 38 | func TestDefaults(t *testing.T) { 39 | var tests = []struct { 40 | msg string 41 | args []string 42 | expected defaultOptions 43 | }{ 44 | { 45 | msg: "no arguments, expecting default values", 46 | args: []string{}, 47 | expected: defaultOptions{ 48 | Int: 0, 49 | IntDefault: 1, 50 | 51 | Float64: 0.0, 52 | Float64Default: -3.14, 53 | 54 | NumericFlag: false, 55 | 56 | String: "", 57 | StringDefault: "abc", 58 | 59 | Time: 0, 60 | TimeDefault: time.Minute, 61 | 62 | Map: map[string]int{}, 63 | MapDefault: map[string]int{"a": 1}, 64 | 65 | Slice: []int{}, 66 | SliceDefault: []int{1, 2}, 67 | }, 68 | }, 69 | { 70 | msg: "non-zero value arguments, expecting overwritten arguments", 71 | args: []string{"--i=3", "--id=3", "--f=-2.71", "--fd=2.71", "-3", "--str=def", "--strd=def", "--t=3ms", "--td=3ms", "--m=c:3", "--md=c:3", "--s=3", "--sd=3"}, 72 | expected: defaultOptions{ 73 | Int: 3, 74 | IntDefault: 3, 75 | 76 | Float64: -2.71, 77 | Float64Default: 2.71, 78 | 79 | NumericFlag: true, 80 | 81 | String: "def", 82 | StringDefault: "def", 83 | 84 | Time: 3 * time.Millisecond, 85 | TimeDefault: 3 * time.Millisecond, 86 | 87 | Map: map[string]int{"c": 3}, 88 | MapDefault: map[string]int{"c": 3}, 89 | 90 | Slice: []int{3}, 91 | SliceDefault: []int{3}, 92 | }, 93 | }, 94 | { 95 | msg: "zero value arguments, expecting overwritten arguments", 96 | args: []string{"--i=0", "--id=0", "--f=0", "--fd=0", "--str", "", "--strd=\"\"", "--t=0ms", "--td=0s", "--m=:0", "--md=:0", "--s=0", "--sd=0"}, 97 | expected: defaultOptions{ 98 | Int: 0, 99 | IntDefault: 0, 100 | 101 | Float64: 0, 102 | Float64Default: 0, 103 | 104 | String: "", 105 | StringDefault: "", 106 | 107 | Time: 0, 108 | TimeDefault: 0, 109 | 110 | Map: map[string]int{"": 0}, 111 | MapDefault: map[string]int{"": 0}, 112 | 113 | Slice: []int{0}, 114 | SliceDefault: []int{0}, 115 | }, 116 | }, 117 | } 118 | 119 | for _, test := range tests { 120 | var opts defaultOptions 121 | 122 | _, err := ParseArgs(&opts, test.args) 123 | if err != nil { 124 | t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) 125 | } 126 | 127 | if opts.Slice == nil { 128 | opts.Slice = []int{} 129 | } 130 | 131 | if !reflect.DeepEqual(opts, test.expected) { 132 | t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) 133 | } 134 | } 135 | } 136 | 137 | func TestNoDefaultsForBools(t *testing.T) { 138 | var opts struct { 139 | DefaultBool bool `short:"d" default:"true"` 140 | } 141 | 142 | if runtime.GOOS == "windows" { 143 | assertParseFail(t, ErrInvalidTag, "boolean flag `/d' may not have default values, they always default to `false' and can only be turned on", &opts) 144 | } else { 145 | assertParseFail(t, ErrInvalidTag, "boolean flag `-d' may not have default values, they always default to `false' and can only be turned on", &opts) 146 | } 147 | } 148 | 149 | func TestUnquoting(t *testing.T) { 150 | var tests = []struct { 151 | arg string 152 | err error 153 | value string 154 | }{ 155 | { 156 | arg: "\"abc", 157 | err: strconv.ErrSyntax, 158 | value: "", 159 | }, 160 | { 161 | arg: "\"\"abc\"", 162 | err: strconv.ErrSyntax, 163 | value: "", 164 | }, 165 | { 166 | arg: "\"abc\"", 167 | err: nil, 168 | value: "abc", 169 | }, 170 | { 171 | arg: "\"\\\"abc\\\"\"", 172 | err: nil, 173 | value: "\"abc\"", 174 | }, 175 | { 176 | arg: "\"\\\"abc\"", 177 | err: nil, 178 | value: "\"abc", 179 | }, 180 | } 181 | 182 | for _, test := range tests { 183 | var opts defaultOptions 184 | 185 | for _, delimiter := range []bool{false, true} { 186 | p := NewParser(&opts, None) 187 | 188 | var err error 189 | if delimiter { 190 | _, err = p.ParseArgs([]string{"--str=" + test.arg, "--strnot=" + test.arg}) 191 | } else { 192 | _, err = p.ParseArgs([]string{"--str", test.arg, "--strnot", test.arg}) 193 | } 194 | 195 | if test.err == nil { 196 | if err != nil { 197 | t.Fatalf("Expected no error but got: %v", err) 198 | } 199 | 200 | if test.value != opts.String { 201 | t.Fatalf("Expected String to be %q but got %q", test.value, opts.String) 202 | } 203 | if q := strconv.Quote(test.value); q != opts.StringNotUnquoted { 204 | t.Fatalf("Expected StringDefault to be %q but got %q", q, opts.StringNotUnquoted) 205 | } 206 | } else { 207 | if err == nil { 208 | t.Fatalf("Expected error") 209 | } else if e, ok := err.(*Error); ok { 210 | if strings.HasPrefix(e.Message, test.err.Error()) { 211 | t.Fatalf("Expected error message to end with %q but got %v", test.err.Error(), e.Message) 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } 218 | 219 | // EnvRestorer keeps a copy of a set of env variables and can restore the env from them 220 | type EnvRestorer struct { 221 | env map[string]string 222 | } 223 | 224 | func (r *EnvRestorer) Restore() { 225 | os.Clearenv() 226 | 227 | for k, v := range r.env { 228 | os.Setenv(k, v) 229 | } 230 | } 231 | 232 | // EnvSnapshot returns a snapshot of the currently set env variables 233 | func EnvSnapshot() *EnvRestorer { 234 | r := EnvRestorer{make(map[string]string)} 235 | 236 | for _, kv := range os.Environ() { 237 | parts := strings.SplitN(kv, "=", 2) 238 | 239 | if len(parts) != 2 { 240 | panic("got a weird env variable: " + kv) 241 | } 242 | 243 | r.env[parts[0]] = parts[1] 244 | } 245 | 246 | return &r 247 | } 248 | 249 | type envNestedOptions struct { 250 | Foo string `long:"foo" default:"z" env:"FOO"` 251 | } 252 | 253 | type envDefaultOptions struct { 254 | Int int `long:"i" default:"1" env:"TEST_I"` 255 | Time time.Duration `long:"t" default:"1m" env:"TEST_T"` 256 | Map map[string]int `long:"m" default:"a:1" env:"TEST_M" env-delim:";"` 257 | Slice []int `long:"s" default:"1" default:"2" env:"TEST_S" env-delim:","` 258 | Nested envNestedOptions `group:"nested" namespace:"nested" env-namespace:"NESTED"` 259 | } 260 | 261 | func TestEnvDefaults(t *testing.T) { 262 | var tests = []struct { 263 | msg string 264 | args []string 265 | expected envDefaultOptions 266 | expectedErr string 267 | env map[string]string 268 | }{ 269 | { 270 | msg: "no arguments, no env, expecting default values", 271 | args: []string{}, 272 | expected: envDefaultOptions{ 273 | Int: 1, 274 | Time: time.Minute, 275 | Map: map[string]int{"a": 1}, 276 | Slice: []int{1, 2}, 277 | Nested: envNestedOptions{ 278 | Foo: "z", 279 | }, 280 | }, 281 | }, 282 | { 283 | msg: "no arguments, env defaults, expecting env default values", 284 | args: []string{}, 285 | expected: envDefaultOptions{ 286 | Int: 2, 287 | Time: 2 * time.Minute, 288 | Map: map[string]int{"a": 2, "b": 3}, 289 | Slice: []int{4, 5, 6}, 290 | Nested: envNestedOptions{ 291 | Foo: "a", 292 | }, 293 | }, 294 | env: map[string]string{ 295 | "TEST_I": "2", 296 | "TEST_T": "2m", 297 | "TEST_M": "a:2;b:3", 298 | "TEST_S": "4,5,6", 299 | "NESTED_FOO": "a", 300 | }, 301 | }, 302 | { 303 | msg: "no arguments, malformed env defaults, expecting parse error", 304 | args: []string{}, 305 | expectedErr: `parsing "two": invalid syntax`, 306 | env: map[string]string{ 307 | "TEST_I": "two", 308 | }, 309 | }, 310 | { 311 | msg: "non-zero value arguments, expecting overwritten arguments", 312 | args: []string{"--i=3", "--t=3ms", "--m=c:3", "--s=3", "--nested.foo=\"p\""}, 313 | expected: envDefaultOptions{ 314 | Int: 3, 315 | Time: 3 * time.Millisecond, 316 | Map: map[string]int{"c": 3}, 317 | Slice: []int{3}, 318 | Nested: envNestedOptions{ 319 | Foo: "p", 320 | }, 321 | }, 322 | env: map[string]string{ 323 | "TEST_I": "2", 324 | "TEST_T": "2m", 325 | "TEST_M": "a:2;b:3", 326 | "TEST_S": "4,5,6", 327 | "NESTED_FOO": "a", 328 | }, 329 | }, 330 | { 331 | msg: "zero value arguments, expecting overwritten arguments", 332 | args: []string{"--i=0", "--t=0ms", "--m=:0", "--s=0", "--nested.foo=\"\""}, 333 | expected: envDefaultOptions{ 334 | Int: 0, 335 | Time: 0, 336 | Map: map[string]int{"": 0}, 337 | Slice: []int{0}, 338 | Nested: envNestedOptions{ 339 | Foo: "", 340 | }, 341 | }, 342 | env: map[string]string{ 343 | "TEST_I": "2", 344 | "TEST_T": "2m", 345 | "TEST_M": "a:2;b:3", 346 | "TEST_S": "4,5,6", 347 | "NESTED_FOO": "a", 348 | }, 349 | }, 350 | } 351 | 352 | oldEnv := EnvSnapshot() 353 | defer oldEnv.Restore() 354 | 355 | for _, test := range tests { 356 | var opts envDefaultOptions 357 | oldEnv.Restore() 358 | for envKey, envValue := range test.env { 359 | os.Setenv(envKey, envValue) 360 | } 361 | _, err := NewParser(&opts, None).ParseArgs(test.args) 362 | if test.expectedErr != "" { 363 | if err == nil { 364 | t.Errorf("%s:\nExpected error containing substring %q", test.msg, test.expectedErr) 365 | } else if !strings.Contains(err.Error(), test.expectedErr) { 366 | t.Errorf("%s:\nExpected error %q to contain substring %q", test.msg, err, test.expectedErr) 367 | } 368 | } else { 369 | if err != nil { 370 | t.Fatalf("%s:\nUnexpected error: %v", test.msg, err) 371 | } 372 | 373 | if opts.Slice == nil { 374 | opts.Slice = []int{} 375 | } 376 | 377 | if !reflect.DeepEqual(opts, test.expected) { 378 | t.Errorf("%s:\nUnexpected options with arguments %+v\nexpected\n%+v\nbut got\n%+v\n", test.msg, test.args, test.expected, opts) 379 | } 380 | } 381 | } 382 | } 383 | 384 | type CustomFlag struct { 385 | Value string 386 | } 387 | 388 | func (c *CustomFlag) UnmarshalFlag(s string) error { 389 | c.Value = s 390 | return nil 391 | } 392 | 393 | func (c *CustomFlag) IsValidValue(s string) error { 394 | if !(s == "-1" || s == "-foo") { 395 | return errors.New("invalid flag value") 396 | } 397 | return nil 398 | } 399 | 400 | func TestOptionAsArgument(t *testing.T) { 401 | var tests = []struct { 402 | args []string 403 | expectError bool 404 | errType ErrorType 405 | errMsg string 406 | rest []string 407 | }{ 408 | { 409 | // short option must not be accepted as argument 410 | args: []string{"--string-slice", "foobar", "--string-slice", "-o"}, 411 | expectError: true, 412 | errType: ErrExpectedArgument, 413 | errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-o'", 414 | }, 415 | { 416 | // long option must not be accepted as argument 417 | args: []string{"--string-slice", "foobar", "--string-slice", "--other-option"}, 418 | expectError: true, 419 | errType: ErrExpectedArgument, 420 | errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `--other-option'", 421 | }, 422 | { 423 | // long option must not be accepted as argument 424 | args: []string{"--string-slice", "--"}, 425 | expectError: true, 426 | errType: ErrExpectedArgument, 427 | errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got double dash `--'", 428 | }, 429 | { 430 | // quoted and appended option should be accepted as argument (even if it looks like an option) 431 | args: []string{"--string-slice", "foobar", "--string-slice=\"--other-option\""}, 432 | }, 433 | { 434 | // Accept any single character arguments including '-' 435 | args: []string{"--string-slice", "-"}, 436 | }, 437 | { 438 | // Do not accept arguments which start with '-' even if the next character is a digit 439 | args: []string{"--string-slice", "-3.14"}, 440 | expectError: true, 441 | errType: ErrExpectedArgument, 442 | errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-3.14'", 443 | }, 444 | { 445 | // Do not accept arguments which start with '-' if the next character is not a digit 446 | args: []string{"--string-slice", "-character"}, 447 | expectError: true, 448 | errType: ErrExpectedArgument, 449 | errMsg: "expected argument for flag `" + defaultLongOptDelimiter + "string-slice', but got option `-character'", 450 | }, 451 | { 452 | args: []string{"-o", "-", "-"}, 453 | rest: []string{"-", "-"}, 454 | }, 455 | { 456 | // Accept arguments which start with '-' if the next character is a digit 457 | args: []string{"--int-slice", "-3"}, 458 | }, 459 | { 460 | // Accept arguments which start with '-' if the next character is a digit 461 | args: []string{"--int16", "-3"}, 462 | }, 463 | { 464 | // Accept arguments which start with '-' if the next character is a digit 465 | args: []string{"--float32", "-3.2"}, 466 | }, 467 | { 468 | // Accept arguments which start with '-' if the next character is a digit 469 | args: []string{"--float32ptr", "-3.2"}, 470 | }, 471 | { 472 | // Accept arguments for values that pass the IsValidValue fuction for value validators 473 | args: []string{"--custom-flag", "-foo"}, 474 | }, 475 | { 476 | // Accept arguments for values that pass the IsValidValue fuction for value validators 477 | args: []string{"--custom-flag", "-1"}, 478 | }, 479 | { 480 | // Rejects arguments for values that fail the IsValidValue fuction for value validators 481 | args: []string{"--custom-flag", "-2"}, 482 | expectError: true, 483 | errType: ErrExpectedArgument, 484 | errMsg: "invalid flag value", 485 | }, 486 | } 487 | 488 | var opts struct { 489 | StringSlice []string `long:"string-slice"` 490 | IntSlice []int `long:"int-slice"` 491 | Int16 int16 `long:"int16"` 492 | Float32 float32 `long:"float32"` 493 | Float32Ptr *float32 `long:"float32ptr"` 494 | OtherOption bool `long:"other-option" short:"o"` 495 | Custom CustomFlag `long:"custom-flag" short:"c"` 496 | } 497 | 498 | for _, test := range tests { 499 | if test.expectError { 500 | assertParseFail(t, test.errType, test.errMsg, &opts, test.args...) 501 | } else { 502 | args := assertParseSuccess(t, &opts, test.args...) 503 | 504 | assertStringArray(t, args, test.rest) 505 | } 506 | } 507 | } 508 | 509 | func TestUnknownFlagHandler(t *testing.T) { 510 | 511 | var opts struct { 512 | Flag1 string `long:"flag1"` 513 | Flag2 string `long:"flag2"` 514 | } 515 | 516 | p := NewParser(&opts, None) 517 | 518 | var unknownFlag1 string 519 | var unknownFlag2 bool 520 | var unknownFlag3 string 521 | 522 | // Set up a callback to intercept unknown options during parsing 523 | p.UnknownOptionHandler = func(option string, arg SplitArgument, args []string) ([]string, error) { 524 | if option == "unknownFlag1" { 525 | if argValue, ok := arg.Value(); ok { 526 | unknownFlag1 = argValue 527 | return args, nil 528 | } 529 | // consume a value from remaining args list 530 | unknownFlag1 = args[0] 531 | return args[1:], nil 532 | } else if option == "unknownFlag2" { 533 | // treat this one as a bool switch, don't consume any args 534 | unknownFlag2 = true 535 | return args, nil 536 | } else if option == "unknownFlag3" { 537 | if argValue, ok := arg.Value(); ok { 538 | unknownFlag3 = argValue 539 | return args, nil 540 | } 541 | // consume a value from remaining args list 542 | unknownFlag3 = args[0] 543 | return args[1:], nil 544 | } 545 | 546 | return args, fmt.Errorf("Unknown flag: %v", option) 547 | } 548 | 549 | // Parse args containing some unknown flags, verify that 550 | // our callback can handle all of them 551 | _, err := p.ParseArgs([]string{"--flag1=stuff", "--unknownFlag1", "blah", "--unknownFlag2", "--unknownFlag3=baz", "--flag2=foo"}) 552 | 553 | if err != nil { 554 | assertErrorf(t, "Parser returned unexpected error %v", err) 555 | } 556 | 557 | assertString(t, opts.Flag1, "stuff") 558 | assertString(t, opts.Flag2, "foo") 559 | assertString(t, unknownFlag1, "blah") 560 | assertString(t, unknownFlag3, "baz") 561 | 562 | if !unknownFlag2 { 563 | assertErrorf(t, "Flag should have been set by unknown handler, but had value: %v", unknownFlag2) 564 | } 565 | 566 | // Parse args with unknown flags that callback doesn't handle, verify it returns error 567 | _, err = p.ParseArgs([]string{"--flag1=stuff", "--unknownFlagX", "blah", "--flag2=foo"}) 568 | 569 | if err == nil { 570 | assertErrorf(t, "Parser should have returned error, but returned nil") 571 | } 572 | } 573 | 574 | func TestChoices(t *testing.T) { 575 | var opts struct { 576 | Choice string `long:"choose" choice:"v1" choice:"v2"` 577 | } 578 | 579 | assertParseFail(t, ErrInvalidChoice, "Invalid value `invalid' for option `"+defaultLongOptDelimiter+"choose'. Allowed values are: v1 or v2", &opts, "--choose", "invalid") 580 | assertParseSuccess(t, &opts, "--choose", "v2") 581 | assertString(t, opts.Choice, "v2") 582 | } 583 | 584 | func TestEmbedded(t *testing.T) { 585 | type embedded struct { 586 | V bool `short:"v"` 587 | } 588 | var opts struct { 589 | embedded 590 | } 591 | 592 | assertParseSuccess(t, &opts, "-v") 593 | 594 | if !opts.V { 595 | t.Errorf("Expected V to be true") 596 | } 597 | } 598 | 599 | type command struct { 600 | } 601 | 602 | func (c *command) Execute(args []string) error { 603 | return nil 604 | } 605 | 606 | func TestCommandHandlerNoCommand(t *testing.T) { 607 | var opts = struct { 608 | Value bool `short:"v"` 609 | }{} 610 | 611 | parser := NewParser(&opts, Default&^PrintErrors) 612 | 613 | var executedCommand Commander 614 | var executedArgs []string 615 | 616 | executed := false 617 | 618 | parser.CommandHandler = func(command Commander, args []string) error { 619 | executed = true 620 | 621 | executedCommand = command 622 | executedArgs = args 623 | 624 | return nil 625 | } 626 | 627 | _, err := parser.ParseArgs([]string{"arg1", "arg2"}) 628 | 629 | if err != nil { 630 | t.Fatalf("Unexpected parse error: %s", err) 631 | } 632 | 633 | if !executed { 634 | t.Errorf("Expected command handler to be executed") 635 | } 636 | 637 | if executedCommand != nil { 638 | t.Errorf("Did not exect an executed command") 639 | } 640 | 641 | assertStringArray(t, executedArgs, []string{"arg1", "arg2"}) 642 | } 643 | 644 | func TestCommandHandler(t *testing.T) { 645 | var opts = struct { 646 | Value bool `short:"v"` 647 | 648 | Command command `command:"cmd"` 649 | }{} 650 | 651 | parser := NewParser(&opts, Default&^PrintErrors) 652 | 653 | var executedCommand Commander 654 | var executedArgs []string 655 | 656 | executed := false 657 | 658 | parser.CommandHandler = func(command Commander, args []string) error { 659 | executed = true 660 | 661 | executedCommand = command 662 | executedArgs = args 663 | 664 | return nil 665 | } 666 | 667 | _, err := parser.ParseArgs([]string{"cmd", "arg1", "arg2"}) 668 | 669 | if err != nil { 670 | t.Fatalf("Unexpected parse error: %s", err) 671 | } 672 | 673 | if !executed { 674 | t.Errorf("Expected command handler to be executed") 675 | } 676 | 677 | if executedCommand == nil { 678 | t.Errorf("Expected command handler to be executed") 679 | } 680 | 681 | assertStringArray(t, executedArgs, []string{"arg1", "arg2"}) 682 | } 683 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestTagMissingColon(t *testing.T) { 8 | var opts = struct { 9 | Value bool `short` 10 | }{} 11 | 12 | assertParseFail(t, ErrTag, "expected `:' after key name, but got end of tag (in `short`)", &opts, "") 13 | } 14 | 15 | func TestTagMissingValue(t *testing.T) { 16 | var opts = struct { 17 | Value bool `short:` 18 | }{} 19 | 20 | assertParseFail(t, ErrTag, "expected `\"' to start tag value at end of tag (in `short:`)", &opts, "") 21 | } 22 | 23 | func TestTagMissingQuote(t *testing.T) { 24 | var opts = struct { 25 | Value bool `short:"v` 26 | }{} 27 | 28 | assertParseFail(t, ErrTag, "expected end of tag value `\"' at end of tag (in `short:\"v`)", &opts, "") 29 | } 30 | 31 | func TestTagNewline(t *testing.T) { 32 | var opts = struct { 33 | Value bool `long:"verbose" description:"verbose 34 | something"` 35 | }{} 36 | 37 | assertParseFail(t, ErrTag, "unexpected newline in tag value `description' (in `long:\"verbose\" description:\"verbose\nsomething\"`)", &opts, "") 38 | } 39 | -------------------------------------------------------------------------------- /termsize.go: -------------------------------------------------------------------------------- 1 | // +build !windows,!plan9,!appengine,!wasm 2 | 3 | package flags 4 | 5 | import ( 6 | "golang.org/x/sys/unix" 7 | ) 8 | 9 | func getTerminalColumns() int { 10 | ws, err := unix.IoctlGetWinsize(0, unix.TIOCGWINSZ) 11 | if err != nil { 12 | return 80 13 | } 14 | return int(ws.Col) 15 | } 16 | -------------------------------------------------------------------------------- /termsize_nosysioctl.go: -------------------------------------------------------------------------------- 1 | // +build plan9 appengine wasm 2 | 3 | package flags 4 | 5 | func getTerminalColumns() int { 6 | return 80 7 | } 8 | -------------------------------------------------------------------------------- /termsize_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package flags 4 | 5 | import ( 6 | "syscall" 7 | "unsafe" 8 | ) 9 | 10 | type ( 11 | SHORT int16 12 | WORD uint16 13 | 14 | SMALL_RECT struct { 15 | Left SHORT 16 | Top SHORT 17 | Right SHORT 18 | Bottom SHORT 19 | } 20 | 21 | COORD struct { 22 | X SHORT 23 | Y SHORT 24 | } 25 | 26 | CONSOLE_SCREEN_BUFFER_INFO struct { 27 | Size COORD 28 | CursorPosition COORD 29 | Attributes WORD 30 | Window SMALL_RECT 31 | MaximumWindowSize COORD 32 | } 33 | ) 34 | 35 | var kernel32DLL = syscall.NewLazyDLL("kernel32.dll") 36 | var getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo") 37 | 38 | func getError(r1, r2 uintptr, lastErr error) error { 39 | // If the function fails, the return value is zero. 40 | if r1 == 0 { 41 | if lastErr != nil { 42 | return lastErr 43 | } 44 | return syscall.EINVAL 45 | } 46 | return nil 47 | } 48 | 49 | func getStdHandle(stdhandle int) (uintptr, error) { 50 | handle, err := syscall.GetStdHandle(stdhandle) 51 | if err != nil { 52 | return 0, err 53 | } 54 | return uintptr(handle), nil 55 | } 56 | 57 | // GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer. 58 | // http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx 59 | func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) { 60 | var info CONSOLE_SCREEN_BUFFER_INFO 61 | if err := getError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0)); err != nil { 62 | return nil, err 63 | } 64 | return &info, nil 65 | } 66 | 67 | func getTerminalColumns() int { 68 | defaultWidth := 80 69 | 70 | stdoutHandle, err := getStdHandle(syscall.STD_OUTPUT_HANDLE) 71 | if err != nil { 72 | return defaultWidth 73 | } 74 | 75 | info, err := GetConsoleScreenBufferInfo(stdoutHandle) 76 | if err != nil { 77 | return defaultWidth 78 | } 79 | 80 | if info.MaximumWindowSize.X > 0 { 81 | return int(info.MaximumWindowSize.X) 82 | } 83 | 84 | return defaultWidth 85 | } 86 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------