├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE.txt ├── README.md ├── circle.yml ├── examples └── readme_example.go ├── examples_test.go ├── flag.go ├── flagset.go ├── goptions.go ├── helpfunc.go ├── marshaler.go ├── marshaler_test.go ├── mutexgroup.go ├── options.go ├── parse_test.go ├── parsetag_test.go ├── special_types.go ├── tagparser.go ├── valueparser.go └── valueparser_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## 2.5.6 3 | ### Bug fixes 4 | 5 | * Unexported fields are now ignored 6 | 7 | ### Minor changes 8 | 9 | * Examples for Verbs and Remainder in documentation 10 | 11 | ## 2.5.4 12 | ### Bugfixes 13 | 14 | * Fix typo in documentation 15 | 16 | ## 2.5.3 17 | ### Bugfixes 18 | 19 | * Remove placeholders from LICENSE 20 | * Add CONTROBUTORS 21 | 22 | ## 2.5.2 23 | ### Bugfixes 24 | 25 | * Bring `examples/readme_example.go` and `README.md` up to date 26 | * Rewrite formatter 27 | 28 | ## 2.5.1 29 | ### Bugfixes 30 | 31 | * Make arrays of `goptions.Marshaler` work 32 | 33 | ## 2.5.0 34 | ### New features 35 | 36 | * Add support for `int32` and `int64` 37 | * Add support for `float32` and `float64` 38 | 39 | ### Bugfixes 40 | 41 | * Fix a bug where the name of a unknown type would not be properly 42 | printed 43 | * Fix checks whether to use `os.Stdin` or `os.Stdout` when "-" is given for a 44 | `*os.File` 45 | * Fix an test example where the output to `os.Stderr` is apparently 46 | not evaluated anymore. 47 | 48 | ## 2.4.1 49 | ### Bugfixes 50 | 51 | * Code was not compilable due to temporary [maintainer](http://github.com/surma) idiocy 52 | (Thanks [akrennmair](http://github.com/akrennmair)) 53 | 54 | ## 2.4.0 55 | ### New features 56 | 57 | * Gave `goptions.FlagSet` a `ParseAndFail()` method 58 | 59 | ## 2.3.0 60 | ### New features 61 | 62 | * Add support for `time.Duration` 63 | 64 | ## 2.2.0 65 | ### New features 66 | 67 | * Add support for `*net.TCPAddr` 68 | * Add support for `*net/url.URL` 69 | 70 | ### Bugfixes 71 | 72 | * Fix behaviour of `[]bool` fields 73 | 74 | ## 2.1.0 75 | ### New features 76 | 77 | * `goptions.Verbs` is of type `string` and will have selected verb name as value 78 | after parsing. 79 | 80 | ## 2.0.0 81 | ### Breaking changes 82 | 83 | * Disallow multiple flag names for one member 84 | * Remove `accumulate` option in favor of generic array support 85 | 86 | ### New features 87 | 88 | * Add convenience function `ParseAndFail` to make common usage of the library 89 | a one-liner (see `readme_example.go`) 90 | * Add a `Marshaler` interface to enable thrid-party types 91 | * Add support for slices (and thereby for mutiple flag definitions) 92 | 93 | ### Minor changes 94 | 95 | * Refactoring to get more flexibility 96 | * Make a flag's default value accessible in the template context 97 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | These people have contributed to goptions's design and implementation: 5 | 6 | * Andreas Krennmair 7 | * GDG Berlin Golang 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013, voxelbrain UG, Germany 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 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 copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the voxelbrain UG nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL voxelbrain UG BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `goptions` implements a flexible parser for command line options. 2 | 3 | Key targets were the support for both long and short flag versions, mutually 4 | exclusive flags, and verbs. Flags and their corresponding variables are defined 5 | by the tags in a (possibly anonymous) struct. 6 | 7 | ![](https://circleci.com/gh/voxelbrain/goptions.png?circle-token=27cd98362d475cfa8c586565b659b2204733f25c) 8 | 9 | # Example 10 | 11 | ```Go 12 | package main 13 | 14 | import ( 15 | "github.com/voxelbrain/goptions" 16 | "os" 17 | "time" 18 | ) 19 | 20 | func main() { 21 | options := struct { 22 | Server string `goptions:"-s, --server, obligatory, description='Server to connect to'"` 23 | Password string `goptions:"-p, --password, description='Don\\'t prompt for password'"` 24 | Timeout time.Duration `goptions:"-t, --timeout, description='Connection timeout in seconds'"` 25 | Help goptions.Help `goptions:"-h, --help, description='Show this help'"` 26 | 27 | goptions.Verbs 28 | Execute struct { 29 | Command string `goptions:"--command, mutexgroup='input', description='Command to exectute', obligatory"` 30 | Script *os.File `goptions:"--script, mutexgroup='input', description='Script to exectute', rdonly"` 31 | } `goptions:"execute"` 32 | Delete struct { 33 | Path string `goptions:"-n, --name, obligatory, description='Name of the entity to be deleted'"` 34 | Force bool `goptions:"-f, --force, description='Force removal'"` 35 | } `goptions:"delete"` 36 | }{ // Default values goes here 37 | Timeout: 10 * time.Second, 38 | } 39 | goptions.ParseAndFail(&options) 40 | } 41 | ``` 42 | 43 | ``` 44 | $ go run examples/readme_example.go --help 45 | Usage: a.out [global options] [verb options] 46 | 47 | Global options: 48 | -s, --server Server to connect to (*) 49 | -p, --password Don't prompt for password 50 | -t, --timeout Connection timeout in seconds (default: 10s) 51 | -h, --help Show this help 52 | 53 | Verbs: 54 | delete: 55 | -n, --name Name of the entity to be deleted (*) 56 | -f, --force Force removal 57 | execute: 58 | --command Command to exectute (*) 59 | --script Script to exectute 60 | ``` 61 | 62 | --- 63 | Version 2.5.6 64 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - go list -f "{{range .TestImports}}{{.}} {{end}}" . | xargs -r go get 4 | - go test 5 | -------------------------------------------------------------------------------- /examples/readme_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/voxelbrain/goptions" 5 | "os" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | options := struct { 11 | Server string `goptions:"-s, --server, obligatory, description='Server to connect to'"` 12 | Password string `goptions:"-p, --password, description='Don\\'t prompt for password'"` 13 | Timeout time.Duration `goptions:"-t, --timeout, description='Connection timeout in seconds'"` 14 | Help goptions.Help `goptions:"-h, --help, description='Show this help'"` 15 | 16 | goptions.Verbs 17 | Execute struct { 18 | Command string `goptions:"--command, mutexgroup='input', description='Command to exectute', obligatory"` 19 | Script *os.File `goptions:"--script, mutexgroup='input', description='Script to exectute', rdonly"` 20 | } `goptions:"execute"` 21 | Delete struct { 22 | Path string `goptions:"-n, --name, obligatory, description='Name of the entity to be deleted'"` 23 | Force bool `goptions:"-f, --force, description='Force removal'"` 24 | } `goptions:"delete"` 25 | }{ // Default values goes here 26 | Timeout: 10 * time.Second, 27 | } 28 | goptions.ParseAndFail(&options) 29 | } 30 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | func ExampleFlagSet_PrintHelp() { 10 | options := struct { 11 | Server string `goptions:"-s, --server, obligatory, description='Server to connect to'"` 12 | Password string `goptions:"-p, --password, description='Don\\'t prompt for password'"` 13 | Timeout time.Duration `goptions:"-t, --timeout, description='Connection timeout in seconds'"` 14 | Help Help `goptions:"-h, --help, description='Show this help'"` 15 | 16 | Verbs 17 | Execute struct { 18 | Command string `goptions:"--command, mutexgroup='input', description='Command to exectute', obligatory"` 19 | Script *os.File `goptions:"--script, mutexgroup='input', description='Script to exectute', rdonly"` 20 | } `goptions:"execute"` 21 | Delete struct { 22 | Path string `goptions:"-n, --name, obligatory, description='Name of the entity to be deleted'"` 23 | Force bool `goptions:"-f, --force, description='Force removal'"` 24 | } `goptions:"delete"` 25 | }{ // Default values goes here 26 | Timeout: 10 * time.Second, 27 | } 28 | 29 | args := []string{"--help"} 30 | fs := NewFlagSet("goptions", &options) 31 | err := fs.Parse(args) 32 | if err == ErrHelpRequest { 33 | fs.PrintHelp(os.Stdout) 34 | return 35 | } else if err != nil { 36 | fmt.Printf("Failure: %s", err) 37 | } 38 | 39 | // Output: 40 | // Usage: goptions [global options] [verb options] 41 | // 42 | // Global options: 43 | // -s, --server Server to connect to (*) 44 | // -p, --password Don't prompt for password 45 | // -t, --timeout Connection timeout in seconds (default: 10s) 46 | // -h, --help Show this help 47 | // 48 | // Verbs: 49 | // delete: 50 | // -n, --name Name of the entity to be deleted (*) 51 | // -f, --force Force removal 52 | // execute: 53 | // --command Command to exectute (*) 54 | // --script Script to exectute 55 | } 56 | 57 | func ExampleVerbs() { 58 | options := struct { 59 | ImportantFlag string `goptions:"-f, --flag, description='Important flag, obligatory'"` 60 | Password string `goptions:"-p, --password, description='Don\\'t prompt for password'"` 61 | Timeout time.Duration `goptions:"-t, --timeout, description='Connection timeout in seconds'"` 62 | Help Help `goptions:"-h, --help, description='Show this help'"` 63 | 64 | Verb Verbs 65 | Execute struct { 66 | Command string `goptions:"--command, mutexgroup='input', description='Command to exectute', obligatory"` 67 | Script *os.File `goptions:"--script, mutexgroup='input', description='Script to exectute', rdonly"` 68 | } `goptions:"execute"` 69 | Delete struct { 70 | Path string `goptions:"-n, --name, obligatory, description='Name of the entity to be deleted'"` 71 | Force bool `goptions:"-f, --force, description='Force removal'"` 72 | } `goptions:"delete"` 73 | }{ // Default values goes here 74 | Timeout: 10 * time.Second, 75 | } 76 | 77 | args := []string{"delete", "-n", "/usr/bin"} 78 | fs := NewFlagSet("goptions", &options) 79 | _ = fs.Parse(args) 80 | // Error handling omitted 81 | fmt.Printf("Selected verb: %s", options.Verb) 82 | 83 | // Output: 84 | // Selected verb: delete 85 | } 86 | 87 | func ExampleRemainder() { 88 | options := struct { 89 | Username string `goptions:"-u, --user, obligatory, description='Name of the user'"` 90 | Remainder Remainder 91 | }{} 92 | 93 | args := []string{"-u", "surma", "some", "more", "args"} 94 | fs := NewFlagSet("goptions", &options) 95 | _ = fs.Parse(args) 96 | // Error handling omitted 97 | fmt.Printf("Remainder: %#v", options.Remainder) 98 | 99 | // Output: 100 | // Remainder: goptions.Remainder{"some", "more", "args"} 101 | } 102 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // Flag represents a single flag of a FlagSet. 10 | type Flag struct { 11 | Short string 12 | Long string 13 | MutexGroups []string 14 | Description string 15 | Obligatory bool 16 | WasSpecified bool 17 | value reflect.Value 18 | optionMeta map[string]interface{} 19 | DefaultValue interface{} 20 | } 21 | 22 | // Return the name of the flag preceding the right amount of dashes. 23 | // The long name is preferred. If no name has been specified, "" 24 | // will be returned. 25 | func (f *Flag) Name() string { 26 | if len(f.Long) > 0 { 27 | return "--" + f.Long 28 | } 29 | if len(f.Short) > 0 { 30 | return "-" + f.Short 31 | } 32 | return "" 33 | } 34 | 35 | // NeedsExtraValue returns true if the flag expects a separate value. 36 | func (f *Flag) NeedsExtraValue() bool { 37 | // Explicit over implicit 38 | if f.value.Type() == reflect.TypeOf(new([]bool)).Elem() || 39 | f.value.Type() == reflect.TypeOf(new(bool)).Elem() { 40 | return false 41 | } 42 | if _, ok := f.value.Interface().(Help); ok { 43 | return false 44 | } 45 | return true 46 | } 47 | 48 | // IsMulti returns true if the flag can be specified multiple times. 49 | func (f *Flag) IsMulti() bool { 50 | if f.value.Kind() == reflect.Slice { 51 | return true 52 | } 53 | return false 54 | } 55 | 56 | func isShort(arg string) bool { 57 | return strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") 58 | } 59 | 60 | func isLong(arg string) bool { 61 | return strings.HasPrefix(arg, "--") 62 | } 63 | 64 | func (f *Flag) Handles(arg string) bool { 65 | return (isShort(arg) && arg[1:2] == f.Short) || 66 | (isLong(arg) && arg[2:] == f.Long) 67 | 68 | } 69 | 70 | func (f *Flag) Parse(args []string) ([]string, error) { 71 | param, value := args[0], "" 72 | if f.NeedsExtraValue() && 73 | (len(args) < 2 || (isShort(param) && len(param) > 2)) { 74 | return args, fmt.Errorf("Flag %s needs an argument", f.Name()) 75 | } 76 | if f.WasSpecified && !f.IsMulti() { 77 | return args, fmt.Errorf("Flag %s can only be specified once", f.Name()) 78 | } 79 | if isShort(param) && len(param) > 2 { 80 | // Short flag cluster 81 | args[0] = "-" + param[2:] 82 | } else if f.NeedsExtraValue() { 83 | value = args[1] 84 | args = args[2:] 85 | } else { 86 | args = args[1:] 87 | } 88 | f.WasSpecified = true 89 | return args, f.setValue(value) 90 | } 91 | -------------------------------------------------------------------------------- /flagset.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "reflect" 9 | "strings" 10 | "sync" 11 | ) 12 | 13 | // A FlagSet represents one set of flags which belong to one particular program. 14 | // A FlagSet is also used to represent a subset of flags belonging to one verb. 15 | type FlagSet struct { 16 | // This HelpFunc will be called when PrintHelp() is called. 17 | HelpFunc 18 | // Name of the program. Might be used by HelpFunc. 19 | Name string 20 | helpFlag *Flag 21 | remainderFlag *Flag 22 | shortMap map[string]*Flag 23 | longMap map[string]*Flag 24 | verbFlag *Flag 25 | // Global option flags 26 | Flags []*Flag 27 | // Verbs and corresponding FlagSets 28 | Verbs map[string]*FlagSet 29 | parent *FlagSet 30 | } 31 | 32 | // NewFlagSet returns a new FlagSet containing all the flags which result from 33 | // parsing the tags of the struct. Said struct as to be passed to the function 34 | // as a pointer. 35 | // If a tag line is erroneous, NewFlagSet() panics as this is considered a 36 | // compile time error rather than a runtme error. 37 | func NewFlagSet(name string, v interface{}) *FlagSet { 38 | structValue := reflect.ValueOf(v) 39 | if structValue.Kind() != reflect.Ptr { 40 | panic("Value type is not a pointer to a struct") 41 | } 42 | structValue = structValue.Elem() 43 | if structValue.Kind() != reflect.Struct { 44 | panic("Value type is not a pointer to a struct") 45 | } 46 | return newFlagset(name, structValue, nil) 47 | } 48 | 49 | // Internal version which skips type checking and takes the "parent"'s 50 | // remainder flag as a parameter. 51 | func newFlagset(name string, structValue reflect.Value, parent *FlagSet) *FlagSet { 52 | var once sync.Once 53 | r := &FlagSet{ 54 | Name: name, 55 | Flags: make([]*Flag, 0), 56 | HelpFunc: DefaultHelpFunc, 57 | parent: parent, 58 | } 59 | 60 | if parent != nil && parent.remainderFlag != nil { 61 | r.remainderFlag = parent.remainderFlag 62 | } 63 | 64 | var i int 65 | // Parse Option fields 66 | for i = 0; i < structValue.Type().NumField(); i++ { 67 | // Skip unexported fields 68 | if StartsWithLowercase(structValue.Type().Field(i).Name) { 69 | continue 70 | } 71 | 72 | fieldValue := structValue.Field(i) 73 | tag := structValue.Type().Field(i).Tag.Get("goptions") 74 | flag, err := parseStructField(fieldValue, tag) 75 | 76 | if err != nil { 77 | panic(fmt.Sprintf("Invalid struct field: %s", err)) 78 | } 79 | if fieldValue.Type().Name() == "Verbs" { 80 | r.verbFlag = flag 81 | break 82 | } 83 | if fieldValue.Type().Name() == "Help" { 84 | r.helpFlag = flag 85 | } 86 | if fieldValue.Type().Name() == "Remainder" && r.remainderFlag == nil { 87 | r.remainderFlag = flag 88 | } 89 | 90 | if len(tag) != 0 { 91 | r.Flags = append(r.Flags, flag) 92 | } 93 | } 94 | 95 | // Parse verb fields 96 | for i++; i < structValue.Type().NumField(); i++ { 97 | once.Do(func() { 98 | r.Verbs = make(map[string]*FlagSet) 99 | }) 100 | fieldValue := structValue.Field(i) 101 | tag := structValue.Type().Field(i).Tag.Get("goptions") 102 | r.Verbs[tag] = newFlagset(tag, fieldValue, r) 103 | } 104 | r.createMaps() 105 | return r 106 | } 107 | 108 | var ( 109 | ErrHelpRequest = errors.New("Request for Help") 110 | ) 111 | 112 | // Parse takes the command line arguments and sets the corresponding values 113 | // in the FlagSet's struct. 114 | func (fs *FlagSet) Parse(args []string) (err error) { 115 | // Parse global flags 116 | for len(args) > 0 { 117 | if !((isLong(args[0]) && fs.hasLongFlag(args[0][2:])) || 118 | (isShort(args[0]) && fs.hasShortFlag(args[0][1:2]))) { 119 | break 120 | } 121 | f := fs.FlagByName(args[0]) 122 | args, err = f.Parse(args) 123 | if err != nil { 124 | return 125 | } 126 | if f == fs.helpFlag && f.WasSpecified { 127 | return ErrHelpRequest 128 | } 129 | } 130 | 131 | // Process verb 132 | if len(args) > 0 { 133 | if verb, ok := fs.Verbs[args[0]]; ok { 134 | fs.verbFlag.value.Set(reflect.ValueOf(Verbs(args[0]))) 135 | err := verb.Parse(args[1:]) 136 | if err != nil { 137 | return err 138 | } 139 | args = args[0:0] 140 | } 141 | } 142 | 143 | // Process remainder 144 | if len(args) > 0 { 145 | if fs.remainderFlag == nil { 146 | return fmt.Errorf("Invalid trailing arguments: %v", args) 147 | } 148 | remainder := reflect.MakeSlice(fs.remainderFlag.value.Type(), len(args), len(args)) 149 | reflect.Copy(remainder, reflect.ValueOf(args)) 150 | fs.remainderFlag.value.Set(remainder) 151 | } 152 | 153 | // Check for unset, obligatory, single Flags 154 | for _, f := range fs.Flags { 155 | if f.Obligatory && !f.WasSpecified && len(f.MutexGroups) == 0 { 156 | return fmt.Errorf("%s must be specified", f.Name()) 157 | } 158 | } 159 | 160 | // Check for multiple set Flags in one mutex group 161 | // Check also for unset, obligatory mutex groups 162 | mgs := fs.MutexGroups() 163 | for _, mg := range mgs { 164 | if !mg.IsValid() { 165 | return fmt.Errorf("Exactly one of %s must be specified", strings.Join(mg.Names(), ", ")) 166 | } 167 | } 168 | return nil 169 | } 170 | 171 | func (fs *FlagSet) createMaps() { 172 | fs.longMap = make(map[string]*Flag) 173 | fs.shortMap = make(map[string]*Flag) 174 | for _, flag := range fs.Flags { 175 | fs.longMap[flag.Long] = flag 176 | fs.shortMap[flag.Short] = flag 177 | } 178 | } 179 | 180 | func (fs *FlagSet) hasLongFlag(fname string) bool { 181 | _, ok := fs.longMap[fname] 182 | return ok 183 | } 184 | 185 | func (fs *FlagSet) hasShortFlag(fname string) bool { 186 | _, ok := fs.shortMap[fname] 187 | return ok 188 | } 189 | 190 | func (fs *FlagSet) FlagByName(fname string) *Flag { 191 | if isShort(fname) && fs.hasShortFlag(fname[1:2]) { 192 | return fs.shortMap[fname[1:2]] 193 | } else if isLong(fname) && fs.hasLongFlag(fname[2:]) { 194 | return fs.longMap[fname[2:]] 195 | } 196 | return nil 197 | } 198 | 199 | // MutexGroups returns a map of Flag lists which contain mutually 200 | // exclusive flags. 201 | func (fs *FlagSet) MutexGroups() map[string]MutexGroup { 202 | r := make(map[string]MutexGroup) 203 | for _, f := range fs.Flags { 204 | for _, mg := range f.MutexGroups { 205 | if len(mg) == 0 { 206 | continue 207 | } 208 | if _, ok := r[mg]; !ok { 209 | r[mg] = make(MutexGroup, 0) 210 | } 211 | r[mg] = append(r[mg], f) 212 | } 213 | } 214 | return r 215 | } 216 | 217 | // Prints the FlagSet's help to the given writer. 218 | func (fs *FlagSet) PrintHelp(w io.Writer) { 219 | fs.HelpFunc(w, fs) 220 | } 221 | 222 | func (fs *FlagSet) ParseAndFail(w io.Writer, args []string) { 223 | err := fs.Parse(args) 224 | if err != nil { 225 | errCode := 0 226 | if err != ErrHelpRequest { 227 | errCode = 1 228 | fmt.Fprintf(w, "Error: %s\n", err) 229 | } 230 | fs.PrintHelp(w) 231 | os.Exit(errCode) 232 | } 233 | } 234 | 235 | func StartsWithLowercase(s string) bool { 236 | if len(s) <= 0 { 237 | return false 238 | } 239 | return strings.ToLower(s)[0] == s[0] 240 | } 241 | -------------------------------------------------------------------------------- /goptions.go: -------------------------------------------------------------------------------- 1 | /* 2 | package goptions implements a flexible parser for command line options. 3 | 4 | Key targets were the support for both long and short flag versions, mutually 5 | exclusive flags, and verbs. Flags and their corresponding variables are defined 6 | by the tags in a (possibly anonymous) struct. 7 | 8 | var options struct { 9 | Name string `goptions:"-n, --name"` 10 | Force bool `goptions:"-f, --force"` 11 | Verbosity int `goptions:"-v, --verbose"` 12 | } 13 | 14 | Short flags can be combined (e.g. `-nfv`). Long flags take their value after a 15 | separating space. The equals notation (`--long-flag=value`) is NOT supported 16 | right now. 17 | 18 | Every member of the struct which is supposed to catch a command line value 19 | has to have a "goptions" tag. The contains the short and long flag names for this 20 | member but can additionally specify any of these options below. 21 | 22 | obligatory - Flag must be specified. Otherwise an error will be returned 23 | when Parse() is called. 24 | description='...' - Set the description for this particular flag. Will be 25 | used by the HelpFunc. 26 | mutexgroup='...' - Add this flag to a MutexGroup. Only one flag of the 27 | ones sharing a MutexGroup can be set. Otherwise an error 28 | will be returned when Parse() is called. If one flag in a 29 | MutexGroup is `obligatory` one flag of the group must be 30 | specified. A flag can be in multiple MutexGroups at once. 31 | 32 | Depending on the type of the struct member, additional options might become available: 33 | 34 | Type: *os.File 35 | The given string is interpreted as a path to a file. If the string is "-" 36 | os.Stdin or os.Stdout will be used. os.Stdin will be returned, if the 37 | `rdonly` flag was set. os.Stdout will be returned, if `wronly` was set. 38 | Available options: 39 | Any combination of create, append, rdonly, wronly, rdwr, 40 | excl, sync, trunc and perm can be specified and correspond directly with 41 | the combination of the homonymous flags in the os package. 42 | 43 | Type: *net.TCPAddr 44 | The given string is interpreted as a tcp address. It is passed to 45 | net.ResolvTCPAddr() with "tcp" as the network type identifier. 46 | 47 | Type: *net/url.URL 48 | The given string is parsed by net/url.Parse() 49 | 50 | Type: time.Duration 51 | The given string is parsed by time.ParseDuration() 52 | 53 | If a member is a slice type, multiple definitions of the flags are possible. For each 54 | specification the underlying type will be used. 55 | 56 | goptions also has support for verbs. Each verb accepts its own set of flags which 57 | take exactly the same tag format as global options. For an usage example of verbs 58 | see the PrintHelp() example. 59 | */ 60 | package goptions 61 | 62 | import ( 63 | "os" 64 | "path/filepath" 65 | ) 66 | 67 | const ( 68 | VERSION = "2.5.6" 69 | ) 70 | 71 | var ( 72 | globalFlagSet *FlagSet 73 | ) 74 | 75 | // ParseAndFail is a convenience function to parse os.Args[1:] and print 76 | // the help if an error occurs. This should cover 90% of this library's 77 | // applications. 78 | func ParseAndFail(v interface{}) { 79 | globalFlagSet = NewFlagSet(filepath.Base(os.Args[0]), v) 80 | globalFlagSet.ParseAndFail(os.Stderr, os.Args[1:]) 81 | } 82 | 83 | // Parse parses the command-line flags from os.Args[1:]. 84 | func Parse(v interface{}) error { 85 | globalFlagSet = NewFlagSet(filepath.Base(os.Args[0]), v) 86 | return globalFlagSet.Parse(os.Args[1:]) 87 | } 88 | 89 | // PrintHelp renders the default help to os.Stderr. 90 | func PrintHelp() { 91 | if globalFlagSet == nil { 92 | panic("Must call Parse() before PrintHelp()") 93 | } 94 | globalFlagSet.PrintHelp(os.Stderr) 95 | } 96 | -------------------------------------------------------------------------------- /helpfunc.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | "text/tabwriter" 7 | "text/template" 8 | ) 9 | 10 | // HelpFunc is the signature of a function responsible for printing the help. 11 | type HelpFunc func(w io.Writer, fs *FlagSet) 12 | 13 | // Generates a new HelpFunc taking a `text/template.Template`-formatted 14 | // string as an argument. The resulting template will be executed with the FlagSet 15 | // as its data. 16 | func NewTemplatedHelpFunc(tpl string) HelpFunc { 17 | var once sync.Once 18 | var t *template.Template 19 | return func(w io.Writer, fs *FlagSet) { 20 | once.Do(func() { 21 | t = template.Must(template.New("helpTemplate").Parse(tpl)) 22 | }) 23 | err := t.Execute(w, fs) 24 | if err != nil { 25 | panic(err) 26 | } 27 | } 28 | } 29 | 30 | const ( 31 | _DEFAULT_HELP = "\xffUsage: {{.Name}} [global options] {{with .Verbs}} [verb options]{{end}}\n" + 32 | "\n" + 33 | "Global options:\xff" + 34 | "{{range .Flags}}" + 35 | "\n\t" + 36 | "\t{{with .Short}}" + "-{{.}}," + "{{end}}" + 37 | "\t{{with .Long}}" + "--{{.}}" + "{{end}}" + 38 | "\t{{.Description}}" + 39 | "{{with .DefaultValue}}" + 40 | " (default: {{.}})" + 41 | "{{end}}" + 42 | "{{if .Obligatory}}" + 43 | " (*)" + 44 | "{{end}}" + 45 | "{{end}}" + 46 | "\xff\n\n{{with .Verbs}}Verbs:\xff" + 47 | "{{range .}}" + 48 | "\xff\n {{.Name}}:\xff" + 49 | "{{range .Flags}}" + 50 | "\n\t" + 51 | "\t{{with .Short}}" + "-{{.}}," + "{{end}}" + 52 | "\t{{with .Long}}" + "--{{.}}" + "{{end}}" + 53 | "\t{{.Description}}" + 54 | "{{with .DefaultValue}}" + 55 | " (default: {{.}})" + 56 | "{{end}}" + 57 | "{{if .Obligatory}}" + 58 | " (*)" + 59 | "{{end}}" + 60 | "{{end}}" + 61 | "{{end}}" + 62 | "{{end}}" + 63 | "\n" 64 | ) 65 | 66 | // DefaultHelpFunc is a HelpFunc which renders the default help template and pipes 67 | // the output through a text/tabwriter.Writer before flushing it to the output. 68 | func DefaultHelpFunc(w io.Writer, fs *FlagSet) { 69 | tw := tabwriter.NewWriter(w, 4, 4, 1, ' ', tabwriter.StripEscape|tabwriter.DiscardEmptyColumns) 70 | NewTemplatedHelpFunc(_DEFAULT_HELP)(tw, fs) 71 | tw.Flush() 72 | } 73 | -------------------------------------------------------------------------------- /marshaler.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | type Marshaler interface { 4 | MarshalGoption(s string) error 5 | } 6 | -------------------------------------------------------------------------------- /marshaler_test.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | type Name struct { 11 | FirstName string 12 | LastName string 13 | } 14 | 15 | func (n *Name) MarshalGoption(val string) error { 16 | f := strings.SplitN(val, " ", 2) 17 | if len(f) != 2 { 18 | return fmt.Errorf("Incomplete name") 19 | } 20 | n.FirstName = f[0] 21 | n.LastName = f[1] 22 | return nil 23 | } 24 | 25 | func TestMarshaler(t *testing.T) { 26 | var args []string 27 | var err error 28 | var fs *FlagSet 29 | var options struct { 30 | Name *Name `goptions:"--name"` 31 | } 32 | args = []string{"--name", "Alexander Surma"} 33 | fs = NewFlagSet("goptions", &options) 34 | err = fs.Parse(args) 35 | if err != nil { 36 | t.Fatalf("Parsing failed: %s", err) 37 | } 38 | expected := &Name{ 39 | FirstName: "Alexander", 40 | LastName: "Surma", 41 | } 42 | if !reflect.DeepEqual(options.Name, expected) { 43 | t.Fatalf("Unexpected value: %#v", options) 44 | } 45 | } 46 | 47 | func TestArrayOfMarshaler(t *testing.T) { 48 | var args []string 49 | var err error 50 | var fs *FlagSet 51 | var options struct { 52 | Names []*Name `goptions:"--name"` 53 | } 54 | args = []string{"--name", "Alexander Surma", "--name", "Yo Mama"} 55 | fs = NewFlagSet("goptions", &options) 56 | err = fs.Parse(args) 57 | if err != nil { 58 | t.Fatalf("Parsing failed: %s", err) 59 | } 60 | expected := []*Name{ 61 | &Name{ 62 | FirstName: "Alexander", 63 | LastName: "Surma", 64 | }, 65 | &Name{ 66 | FirstName: "Yo", 67 | LastName: "Mama", 68 | }, 69 | } 70 | if !reflect.DeepEqual(options.Names, expected) { 71 | t.Fatalf("Unexpected value: %#v", options) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /mutexgroup.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | // A MutexGroup holds a set of flags which are mutually exclusive and cannot 4 | // be specified at the same time. 5 | type MutexGroup []*Flag 6 | 7 | // IsObligatory returns true if exactly one of the flags in the MutexGroup has 8 | // to be specified 9 | func (mg MutexGroup) IsObligatory() bool { 10 | for _, flag := range mg { 11 | if flag.Obligatory { 12 | return true 13 | } 14 | } 15 | return false 16 | } 17 | 18 | func (mg MutexGroup) WasSpecified() bool { 19 | for _, flag := range mg { 20 | if flag.WasSpecified { 21 | return true 22 | } 23 | } 24 | return false 25 | } 26 | 27 | // IsValid checks if the flags in the MutexGroup describe a valid state. 28 | // I.e. At most one has been specified or – if it is an obligatory MutexGroup – 29 | // exactly one has been specified. 30 | func (mg MutexGroup) IsValid() bool { 31 | c := 0 32 | for _, flag := range mg { 33 | if flag.WasSpecified { 34 | c++ 35 | } 36 | } 37 | return c <= 1 && (!mg.IsObligatory() || c == 1) 38 | } 39 | 40 | // Names is a convenience function to return the array of names of the flags 41 | // in the MutexGroup. 42 | func (mg MutexGroup) Names() []string { 43 | r := make([]string, len(mg)) 44 | for i, flag := range mg { 45 | r[i] = flag.Name() 46 | } 47 | return r 48 | } 49 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type optionFunc func(f *Flag, option, value string) error 12 | type optionMap map[string]optionFunc 13 | 14 | var ( 15 | typeOptionMap = map[reflect.Type]optionMap{ 16 | // Global options 17 | nil: optionMap{ 18 | "description": description, 19 | "obligatory": obligatory, 20 | "mutexgroup": mutexgroup, 21 | }, 22 | reflect.TypeOf(new(*os.File)).Elem(): optionMap{ 23 | "create": initOptionMeta(file_create, "file_mode", 0), 24 | "append": initOptionMeta(file_append, "file_mode", 0), 25 | "rdonly": initOptionMeta(file_rdonly, "file_mode", 0), 26 | "wronly": initOptionMeta(file_wronly, "file_mode", 0), 27 | "rdwr": initOptionMeta(file_rdwr, "file_mode", 0), 28 | "excl": initOptionMeta(file_excl, "file_mode", 0), 29 | "sync": initOptionMeta(file_sync, "file_mode", 0), 30 | "trunc": initOptionMeta(file_trunc, "file_mode", 0), 31 | "perm": file_perm, 32 | }, 33 | } 34 | ) 35 | 36 | // Wraps another optionFunc and inits optionMeta[field] with value if it does 37 | // not have one already. 38 | func initOptionMeta(fn optionFunc, field string, init_value interface{}) optionFunc { 39 | return func(f *Flag, option, value string) error { 40 | if _, ok := f.optionMeta[field]; !ok { 41 | f.optionMeta[field] = init_value 42 | } 43 | return fn(f, option, value) 44 | } 45 | } 46 | 47 | func description(f *Flag, option, value string) error { 48 | f.Description = strings.Replace(value, `\`, ``, -1) 49 | return nil 50 | } 51 | 52 | func obligatory(f *Flag, option, value string) error { 53 | f.Obligatory = true 54 | return nil 55 | } 56 | 57 | func mutexgroup(f *Flag, option, value string) error { 58 | if len(value) <= 0 { 59 | return fmt.Errorf("Mutexgroup option needs a value") 60 | } 61 | for _, group := range strings.Split(value, ",") { 62 | f.MutexGroups = append(f.MutexGroups, group) 63 | } 64 | return nil 65 | } 66 | 67 | func file_create(f *Flag, option, value string) error { 68 | f.optionMeta["file_mode"] = f.optionMeta["file_mode"].(int) | os.O_CREATE 69 | return nil 70 | } 71 | 72 | func file_append(f *Flag, option, value string) error { 73 | f.optionMeta["file_mode"] = f.optionMeta["file_mode"].(int) | os.O_APPEND 74 | return nil 75 | } 76 | 77 | func file_rdonly(f *Flag, option, value string) error { 78 | f.optionMeta["file_mode"] = f.optionMeta["file_mode"].(int) | os.O_RDONLY 79 | return nil 80 | } 81 | 82 | func file_wronly(f *Flag, option, value string) error { 83 | f.optionMeta["file_mode"] = f.optionMeta["file_mode"].(int) | os.O_WRONLY 84 | return nil 85 | } 86 | 87 | func file_rdwr(f *Flag, option, value string) error { 88 | f.optionMeta["file_mode"] = f.optionMeta["file_mode"].(int) | os.O_RDWR 89 | return nil 90 | } 91 | 92 | func file_excl(f *Flag, option, value string) error { 93 | f.optionMeta["file_mode"] = f.optionMeta["file_mode"].(int) | os.O_EXCL 94 | return nil 95 | } 96 | 97 | func file_sync(f *Flag, option, value string) error { 98 | f.optionMeta["file_mode"] = f.optionMeta["file_mode"].(int) | os.O_SYNC 99 | return nil 100 | } 101 | 102 | func file_trunc(f *Flag, option, value string) error { 103 | f.optionMeta["file_mode"] = f.optionMeta["file_mode"].(int) | os.O_TRUNC 104 | return nil 105 | } 106 | 107 | func file_perm(f *Flag, option, value string) error { 108 | perm, err := strconv.ParseInt(value, 8, 32) 109 | if err != nil { 110 | return err 111 | } 112 | f.optionMeta["file_perm"] = uint32(perm) 113 | return nil 114 | } 115 | 116 | func optionMapForType(t reflect.Type) optionMap { 117 | g := typeOptionMap[nil] 118 | m, _ := typeOptionMap[t] 119 | r := make(optionMap) 120 | for k, v := range g { 121 | r[k] = v 122 | } 123 | for k, v := range m { 124 | r[k] = v 125 | } 126 | return r 127 | } 128 | -------------------------------------------------------------------------------- /parse_test.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestParse_StringValue(t *testing.T) { 9 | var args []string 10 | var err error 11 | var fs *FlagSet 12 | var options struct { 13 | Name string `goptions:"--name, -n"` 14 | } 15 | expected := "SomeName" 16 | 17 | args = []string{"--name", "SomeName"} 18 | fs = NewFlagSet("goptions", &options) 19 | err = fs.Parse(args) 20 | if err != nil { 21 | t.Fatalf("Flag parsing failed: %s", err) 22 | } 23 | if options.Name != expected { 24 | t.Fatalf("Expected %s for options.Name, got %s", expected, options.Name) 25 | } 26 | 27 | options.Name = "" 28 | 29 | args = []string{"-n", "SomeName"} 30 | fs = NewFlagSet("goptions", &options) 31 | err = fs.Parse(args) 32 | if err != nil { 33 | t.Fatalf("Flag parsing failed: %s", err) 34 | } 35 | if options.Name != expected { 36 | t.Fatalf("Expected %s for options.Name, got %s", expected, options.Name) 37 | } 38 | } 39 | 40 | func TestParse_ObligatoryStringValue(t *testing.T) { 41 | var args []string 42 | var err error 43 | var fs *FlagSet 44 | var options struct { 45 | Name string `goptions:"-n, obligatory"` 46 | } 47 | args = []string{} 48 | fs = NewFlagSet("goptions", &options) 49 | err = fs.Parse(args) 50 | if err == nil { 51 | t.Fatalf("Parsing should have failed.") 52 | } 53 | 54 | args = []string{"-n", "SomeName"} 55 | fs = NewFlagSet("goptions", &options) 56 | err = fs.Parse(args) 57 | if err != nil { 58 | t.Fatalf("Parsing failed: %s", err) 59 | } 60 | 61 | expected := "SomeName" 62 | if options.Name != expected { 63 | t.Fatalf("Expected %s for options.Name, got %s", expected, options.Name) 64 | } 65 | } 66 | 67 | func TestParse_UnknownFlag(t *testing.T) { 68 | var args []string 69 | var err error 70 | var fs *FlagSet 71 | var options struct { 72 | Name string `goptions:"--name, -n"` 73 | } 74 | args = []string{"-k", "4"} 75 | fs = NewFlagSet("goptions", &options) 76 | err = fs.Parse(args) 77 | if err == nil { 78 | t.Fatalf("Parsing should have failed.") 79 | } 80 | } 81 | 82 | func TestParse_FlagCluster(t *testing.T) { 83 | var args []string 84 | var err error 85 | var fs *FlagSet 86 | var options struct { 87 | Fast bool `goptions:"-f"` 88 | Silent bool `goptions:"-q"` 89 | Serious bool `goptions:"-s"` 90 | Crazy bool `goptions:"-c"` 91 | Verbose bool `goptions:"-v"` 92 | } 93 | args = []string{"-fqcv"} 94 | fs = NewFlagSet("goptions", &options) 95 | err = fs.Parse(args) 96 | if err != nil { 97 | t.Fatalf("Parsing failed: %s", err) 98 | } 99 | 100 | if !(options.Fast && 101 | options.Silent && 102 | !options.Serious && 103 | options.Crazy && 104 | options.Verbose) { 105 | t.Fatalf("Unexpected value: %v", options) 106 | } 107 | } 108 | 109 | func TestParse_MutexGroup(t *testing.T) { 110 | var args []string 111 | var err error 112 | var fs *FlagSet 113 | var options struct { 114 | Create bool `goptions:"--create, mutexgroup='action'"` 115 | Delete bool `goptions:"--delete, mutexgroup='action'"` 116 | } 117 | args = []string{"--create", "--delete"} 118 | fs = NewFlagSet("goptions", &options) 119 | err = fs.Parse(args) 120 | if err == nil { 121 | t.Fatalf("Parsing should have failed.") 122 | } 123 | } 124 | 125 | func TestParse_HelpFlag(t *testing.T) { 126 | var args []string 127 | var err error 128 | var fs *FlagSet 129 | var options struct { 130 | Name string `goptions:"--name, -n"` 131 | Help `goptions:"--help, -h"` 132 | } 133 | args = []string{"-n", "SomeNone", "-h"} 134 | fs = NewFlagSet("goptions", &options) 135 | err = fs.Parse(args) 136 | if err != ErrHelpRequest { 137 | t.Fatalf("Expected ErrHelpRequest, got: %s", err) 138 | } 139 | 140 | args = []string{"-n", "SomeNone"} 141 | fs = NewFlagSet("goptions", &options) 142 | err = fs.Parse(args) 143 | if err != nil { 144 | t.Fatalf("Unexpected error returned: %s", err) 145 | } 146 | } 147 | 148 | func TestParse_Verbs(t *testing.T) { 149 | var args []string 150 | var err error 151 | var fs *FlagSet 152 | var options struct { 153 | Server string `goptions:"--server, -s"` 154 | 155 | Verbs 156 | Create struct { 157 | Name string `goptions:"--name, -n"` 158 | } `goptions:"create"` 159 | } 160 | 161 | args = []string{"-s", "127.0.0.1", "create", "-n", "SomeDocument"} 162 | fs = NewFlagSet("goptions", &options) 163 | err = fs.Parse(args) 164 | if err != nil { 165 | t.Fatalf("Parsing failed: %s", err) 166 | } 167 | 168 | if !(options.Server == "127.0.0.1" && 169 | options.Create.Name == "SomeDocument" && 170 | options.Verbs == "create") { 171 | t.Fatalf("Unexpected value: %v", options) 172 | } 173 | } 174 | 175 | func TestParse_IntValue(t *testing.T) { 176 | var args []string 177 | var err error 178 | var fs *FlagSet 179 | var options struct { 180 | Limit int `goptions:"-l"` 181 | } 182 | 183 | args = []string{"-l", "123"} 184 | fs = NewFlagSet("goptions", &options) 185 | err = fs.Parse(args) 186 | if err != nil { 187 | t.Fatalf("Parsing failed: %s", err) 188 | } 189 | 190 | if !(options.Limit == 123) { 191 | t.Fatalf("Unexpected value: %v", options) 192 | } 193 | } 194 | 195 | func TestParse_Remainder(t *testing.T) { 196 | var args []string 197 | var err error 198 | var fs *FlagSet 199 | var options struct { 200 | Limit int `goptions:"-l"` 201 | Remainder 202 | } 203 | 204 | args = []string{"-l", "123", "Something", "SomethingElse"} 205 | fs = NewFlagSet("goptions", &options) 206 | err = fs.Parse(args) 207 | if err != nil { 208 | t.Fatalf("Parsing failed: %s", err) 209 | } 210 | 211 | if !(len(options.Remainder) == 2 && 212 | options.Remainder[0] == "Something" && 213 | options.Remainder[1] == "SomethingElse") { 214 | t.Fatalf("Unexpected value: %v", options) 215 | } 216 | } 217 | 218 | func TestParse_VerbRemainder(t *testing.T) { 219 | var args []string 220 | var err error 221 | var fs *FlagSet 222 | var options struct { 223 | Limit int `goptions:"-l"` 224 | Remainder 225 | 226 | Verbs 227 | Create struct { 228 | Fast bool `goptions:"-f"` 229 | Remainder 230 | } `goptions:"create"` 231 | } 232 | 233 | args = []string{"create", "-f", "Something", "SomethingElse"} 234 | fs = NewFlagSet("goptions", &options) 235 | err = fs.Parse(args) 236 | if err != nil { 237 | t.Fatalf("Parsing failed: %s", err) 238 | } 239 | 240 | if !(len(options.Remainder) == 2 && 241 | options.Remainder[0] == "Something" && 242 | options.Remainder[1] == "SomethingElse" && 243 | options.Verbs == "create") { 244 | t.Fatalf("Unexpected value: %v", options) 245 | } 246 | } 247 | 248 | func TestParse_NoRemainder(t *testing.T) { 249 | var args []string 250 | var err error 251 | var fs *FlagSet 252 | var options struct { 253 | Fast bool `goptions:"-f"` 254 | } 255 | 256 | args = []string{"-f", "Something", "SomethingElse"} 257 | fs = NewFlagSet("goptions", &options) 258 | err = fs.Parse(args) 259 | if err == nil { 260 | t.Fatalf("Parsing should have failed") 261 | } 262 | } 263 | 264 | func TestParse_MissingValue(t *testing.T) { 265 | var args []string 266 | var err error 267 | var fs *FlagSet 268 | var options struct { 269 | Name string `goptions:"-n, --name"` 270 | } 271 | 272 | args = []string{"-n"} 273 | fs = NewFlagSet("goptions", &options) 274 | err = fs.Parse(args) 275 | if err == nil { 276 | t.Fatalf("Parsing should have failed") 277 | } 278 | 279 | args = []string{"--name"} 280 | fs = NewFlagSet("goptions", &options) 281 | err = fs.Parse(args) 282 | if err == nil { 283 | t.Fatalf("Parsing should have failed") 284 | } 285 | } 286 | 287 | func TestParse_ObligatoryMutexGroup(t *testing.T) { 288 | var args []string 289 | var err error 290 | var fs *FlagSet 291 | var options struct { 292 | Create bool `goptions:"-c, mutexgroup='action', obligatory"` 293 | Delete bool `goptions:"-d, mutexgroup='action'"` 294 | } 295 | 296 | args = []string{} 297 | fs = NewFlagSet("goptions", &options) 298 | err = fs.Parse(args) 299 | if err == nil { 300 | t.Fatalf("Parsing should have failed") 301 | } 302 | 303 | args = []string{"-c", "-d"} 304 | fs = NewFlagSet("goptions", &options) 305 | err = fs.Parse(args) 306 | if err == nil { 307 | t.Fatalf("Parsing should have failed") 308 | } 309 | 310 | args = []string{"-d"} 311 | fs = NewFlagSet("goptions", &options) 312 | err = fs.Parse(args) 313 | if err != nil { 314 | t.Fatalf("Parsing failed: %s", err) 315 | } 316 | } 317 | 318 | func TestParse_StringArray_Short(t *testing.T) { 319 | var args []string 320 | var err error 321 | var fs *FlagSet 322 | var options struct { 323 | Servers []string `goptions:"-s"` 324 | } 325 | 326 | args = []string{} 327 | for i := 1; i < 10; i++ { 328 | options.Servers = []string{} 329 | args = append(args, []string{"-s", fmt.Sprintf("server%d", i)}...) 330 | fs = NewFlagSet("goptions", &options) 331 | err = fs.Parse(args) 332 | if err != nil { 333 | t.Fatalf("Parsing failed at %d: %s", i, err) 334 | } 335 | if len(options.Servers) != i { 336 | t.Fatalf("Unexpected number of values. Expected %d, got %d (%#v)", i, len(options.Servers), options.Servers) 337 | } 338 | for j := 0; j < i; j++ { 339 | expected := fmt.Sprintf("server%d", j+1) 340 | if options.Servers[j] != expected { 341 | t.Fatalf("Unexpected value. %#v", options.Servers) 342 | } 343 | } 344 | } 345 | } 346 | 347 | func TestParse_BoolArray_Cluster(t *testing.T) { 348 | var err error 349 | var fs *FlagSet 350 | var options struct { 351 | Verbosity []bool `goptions:"-v"` 352 | } 353 | 354 | args := "-v" 355 | for i := 1; i < 10; i++ { 356 | options.Verbosity = []bool{} 357 | fs = NewFlagSet("goptions", &options) 358 | err = fs.Parse([]string{args}) 359 | if err != nil { 360 | t.Fatalf("Parsing failed at %d: %s", i, err) 361 | } 362 | if len(options.Verbosity) != i { 363 | t.Fatalf("Unexpected number of values. Expected %d, got %d (%#v)", i, len(options.Verbosity), options.Verbosity) 364 | } 365 | args += "v" 366 | } 367 | } 368 | 369 | func TestParse_BoolArray_Short(t *testing.T) { 370 | var args []string 371 | var err error 372 | var fs *FlagSet 373 | var options struct { 374 | Verbosity []bool `goptions:"-v"` 375 | } 376 | 377 | args = []string{} 378 | for i := 1; i < 10; i++ { 379 | options.Verbosity = []bool{} 380 | args = append(args, "-v") 381 | fs = NewFlagSet("goptions", &options) 382 | err = fs.Parse(args) 383 | if err != nil { 384 | t.Fatalf("Parsing failed at %d: %s", i, err) 385 | } 386 | if len(options.Verbosity) != i { 387 | t.Fatalf("Unexpected number of values. Expected %d, got %d (%#v)", i, len(options.Verbosity), options.Verbosity) 388 | } 389 | } 390 | } 391 | 392 | func TestParse_BoolArray_Long(t *testing.T) { 393 | var args []string 394 | var err error 395 | var fs *FlagSet 396 | var options struct { 397 | Verbosity []bool `goptions:"--verbose"` 398 | } 399 | 400 | args = []string{} 401 | for i := 1; i < 10; i++ { 402 | options.Verbosity = []bool{} 403 | args = append(args, "--verbose") 404 | fs = NewFlagSet("goptions", &options) 405 | err = fs.Parse(args) 406 | if err != nil { 407 | t.Fatalf("Parsing failed at %d: %s", i, err) 408 | } 409 | if len(options.Verbosity) != i { 410 | t.Fatalf("Unexpected number of values. Expected %d, got %d (%#v)", i, len(options.Verbosity), options.Verbosity) 411 | } 412 | } 413 | } 414 | 415 | func TestParse_UnexportedVerbs(t *testing.T) { 416 | var options struct { 417 | Verbs 418 | A struct { 419 | A1 string `goptions:"--a1"` 420 | a2 string `goptions:"--a2"` 421 | } `goptions:"A"` 422 | } 423 | args := []string{"A", "--a1", "x"} 424 | fs := NewFlagSet("goptions", &options) 425 | err := fs.Parse(args) 426 | if err != nil { 427 | t.Fatalf("Parsing failed: %s", err) 428 | } 429 | if options.A.A1 != "x" || options.A.a2 != "" { 430 | t.Fatalf("Unexpected values in struct: %#v", options) 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /parsetag_test.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseTag_Minimal(t *testing.T) { 9 | var tag string 10 | tag = `--name, -n, description='Some name'` 11 | f, e := parseStructField(reflect.ValueOf(string("")), tag) 12 | if e != nil { 13 | t.Fatalf("Tag parsing failed: %s", e) 14 | } 15 | expected := &Flag{ 16 | Long: "name", 17 | Short: "n", 18 | Description: "Some name", 19 | } 20 | if !flagequal(f, expected) { 21 | t.Fatalf("Expected %#v, got %#v", expected, f) 22 | } 23 | } 24 | 25 | func TestParseTag_More(t *testing.T) { 26 | var tag string 27 | tag = `--name, -n, description='Some name', mutexgroup='selector', obligatory` 28 | f, e := parseStructField(reflect.ValueOf(string("")), tag) 29 | if e != nil { 30 | t.Fatalf("Tag parsing failed: %s", e) 31 | } 32 | expected := &Flag{ 33 | Long: "name", 34 | Short: "n", 35 | Description: "Some name", 36 | MutexGroups: []string{"selector"}, 37 | Obligatory: true, 38 | } 39 | if !flagequal(f, expected) { 40 | t.Fatalf("Expected %#v, got %#v", expected, f) 41 | } 42 | } 43 | 44 | func TestParseTag_MultipleFlags(t *testing.T) { 45 | var tag string 46 | var e error 47 | tag = `--name1, --name2` 48 | _, e = parseStructField(reflect.ValueOf(string("")), tag) 49 | if e == nil { 50 | t.Fatalf("Parsing should have failed") 51 | } 52 | 53 | tag = `-n, -v` 54 | _, e = parseStructField(reflect.ValueOf(string("")), tag) 55 | if e == nil { 56 | t.Fatalf("Parsing should have failed") 57 | } 58 | } 59 | 60 | func flagequal(f1, f2 *Flag) bool { 61 | return f1.Short == f2.Short && 62 | f1.Long == f2.Long && 63 | reflect.DeepEqual(f1.MutexGroups, f2.MutexGroups) && 64 | f1.Description == f2.Description && 65 | f1.Obligatory == f2.Obligatory && 66 | f1.WasSpecified == f2.WasSpecified 67 | } 68 | -------------------------------------------------------------------------------- /special_types.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | // Help Defines the common help flag. It is handled separately as it will cause 4 | // Parse() to return ErrHelpRequest. 5 | type Help bool 6 | 7 | // Verbs marks the point in the struct where the verbs start. Its value will be 8 | // the name of the selected verb. 9 | type Verbs string 10 | 11 | // A remainder catches all excessive arguments. If both a verb and 12 | // the containing options struct have a remainder field, only the latter one 13 | // will be used. 14 | type Remainder []string 15 | -------------------------------------------------------------------------------- /tagparser.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | _LONG_FLAG_REGEXP = `--[[:word:]-]+` 12 | _SHORT_FLAG_REGEXP = `-[[:alnum:]]` 13 | _QUOTED_STRING_REGEXP = `'((?:\\'|[^\\'])+)'` 14 | _OPTION_REGEXP = `([[:word:]-]+)(?:=` + _QUOTED_STRING_REGEXP + `)?` 15 | ) 16 | 17 | var ( 18 | optionRegexp = regexp.MustCompile(`^(` + strings.Join([]string{_SHORT_FLAG_REGEXP, _LONG_FLAG_REGEXP, _OPTION_REGEXP}, "|") + `)(?:,|$)`) 19 | ) 20 | 21 | func parseStructField(fieldValue reflect.Value, tag string) (*Flag, error) { 22 | f := &Flag{ 23 | value: fieldValue, 24 | DefaultValue: fieldValue.Interface(), 25 | optionMeta: make(map[string]interface{}), 26 | } 27 | for { 28 | tag = strings.TrimSpace(tag) 29 | if len(tag) == 0 { 30 | break 31 | } 32 | idx := optionRegexp.FindStringSubmatchIndex(tag) 33 | if idx == nil { 34 | return nil, fmt.Errorf("Could not find a valid flag definition at the beginning of \"%s\"", tag) 35 | } 36 | option := tag[idx[2]:idx[3]] 37 | 38 | if strings.HasPrefix(option, "--") { 39 | if f.Long != "" { 40 | return nil, fmt.Errorf("Multiple flags assigned to a member: %s", strings.Join([]string{"--" + f.Long, option}, ", ")) 41 | } 42 | f.Long = option[2:] 43 | } else if strings.HasPrefix(option, "-") { 44 | if f.Short != "" { 45 | return nil, fmt.Errorf("Multiple flags assigned to a member: %s", strings.Join([]string{"-" + f.Short, option}, ", ")) 46 | } 47 | f.Short = option[1:] 48 | } else { 49 | option := tag[idx[4]:idx[5]] 50 | value := "" 51 | if idx[6] != -1 { 52 | value = tag[idx[6]:idx[7]] 53 | } 54 | optionmap := optionMapForType(fieldValue.Type()) 55 | opf, ok := optionmap[option] 56 | if !ok { 57 | return nil, fmt.Errorf("Unknown option %s", option) 58 | } 59 | err := opf(f, option, value) 60 | if err != nil { 61 | return nil, fmt.Errorf("Option %s invalid: %s", option, err) 62 | } 63 | } 64 | // Keep remainder 65 | tag = tag[idx[1]:] 66 | } 67 | return f, nil 68 | } 69 | -------------------------------------------------------------------------------- /valueparser.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | "os" 8 | "reflect" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | type valueParser func(f *Flag, val string) (reflect.Value, error) 14 | 15 | var ( 16 | parserMap = map[reflect.Type]valueParser{ 17 | reflect.TypeOf(new(bool)).Elem(): boolValueParser, 18 | reflect.TypeOf(new(string)).Elem(): stringValueParser, 19 | reflect.TypeOf(new(float64)).Elem(): float64ValueParser, 20 | reflect.TypeOf(new(float32)).Elem(): float32ValueParser, 21 | reflect.TypeOf(new(int)).Elem(): intValueParser, 22 | reflect.TypeOf(new(int64)).Elem(): int64ValueParser, 23 | reflect.TypeOf(new(int32)).Elem(): int32ValueParser, 24 | reflect.TypeOf(new(Help)).Elem(): helpValueParser, 25 | reflect.TypeOf(new(*os.File)).Elem(): fileValueParser, 26 | reflect.TypeOf(new(*net.TCPAddr)).Elem(): tcpAddrValueParser, 27 | reflect.TypeOf(new(*url.URL)).Elem(): urlValueParser, 28 | reflect.TypeOf(new(time.Duration)).Elem(): durationValueParser, 29 | } 30 | ) 31 | 32 | func parseMarshalValue(value reflect.Value, s string) error { 33 | newval := reflect.New(value.Type()).Elem() 34 | if newval.Kind() == reflect.Ptr { 35 | newptrval := reflect.New(value.Type().Elem()) 36 | newval.Set(newptrval) 37 | } 38 | err := newval.Interface().(Marshaler).MarshalGoption(s) 39 | value.Set(newval) 40 | return err 41 | } 42 | 43 | func (f *Flag) setValue(s string) (err error) { 44 | defer func() { 45 | if x := recover(); x != nil { 46 | err = x.(error) 47 | return 48 | } 49 | }() 50 | if f.value.Type().Implements(reflect.TypeOf(new(Marshaler)).Elem()) { 51 | return parseMarshalValue(f.value, s) 52 | } 53 | vtype := f.value.Type() 54 | newval := reflect.New(vtype).Elem() 55 | if f.value.Kind() == reflect.Slice { 56 | vtype = f.value.Type().Elem() 57 | if vtype.Implements(reflect.TypeOf(new(Marshaler)).Elem()) { 58 | newval = reflect.New(vtype).Elem() 59 | err := parseMarshalValue(newval, s) 60 | f.value.Set(reflect.Append(f.value, newval)) 61 | return err 62 | } 63 | } 64 | if parser, ok := parserMap[vtype]; ok { 65 | val, err := parser(f, s) 66 | if err != nil { 67 | return err 68 | } 69 | if f.value.Kind() == reflect.Slice { 70 | f.value.Set(reflect.Append(f.value, val)) 71 | } else { 72 | f.value.Set(val) 73 | } 74 | return nil 75 | } else { 76 | return fmt.Errorf("Unsupported flag type: %s", f.value.Type()) 77 | } 78 | panic("Invalid execution path") 79 | } 80 | 81 | func boolValueParser(f *Flag, val string) (reflect.Value, error) { 82 | return reflect.ValueOf(true), nil 83 | } 84 | 85 | func stringValueParser(f *Flag, val string) (reflect.Value, error) { 86 | return reflect.ValueOf(val), nil 87 | } 88 | 89 | func float64ValueParser(f *Flag, val string) (reflect.Value, error) { 90 | floatval, err := strconv.ParseFloat(val, 64) 91 | return reflect.ValueOf(float64(floatval)), err 92 | } 93 | 94 | func float32ValueParser(f *Flag, val string) (reflect.Value, error) { 95 | floatval, err := strconv.ParseFloat(val, 32) 96 | return reflect.ValueOf(float32(floatval)), err 97 | } 98 | 99 | func int64ValueParser(f *Flag, val string) (reflect.Value, error) { 100 | intval, err := strconv.ParseInt(val, 10, 64) 101 | return reflect.ValueOf(int64(intval)), err 102 | } 103 | 104 | func int32ValueParser(f *Flag, val string) (reflect.Value, error) { 105 | intval, err := strconv.ParseInt(val, 10, 32) 106 | return reflect.ValueOf(int32(intval)), err 107 | } 108 | 109 | func intValueParser(f *Flag, val string) (reflect.Value, error) { 110 | intval, err := strconv.ParseInt(val, 10, 64) 111 | return reflect.ValueOf(int(intval)), err 112 | } 113 | 114 | func fileValueParser(f *Flag, val string) (reflect.Value, error) { 115 | mode := 0 116 | if v, ok := f.optionMeta["file_mode"]; ok { 117 | mode = v.(int) 118 | } 119 | if val == "-" { 120 | if mode&os.O_RDONLY == os.O_RDONLY { 121 | return reflect.ValueOf(os.Stdin), nil 122 | } else if mode&os.O_WRONLY == os.O_WRONLY { 123 | return reflect.ValueOf(os.Stdout), nil 124 | } 125 | } else { 126 | perm := uint32(0644) 127 | if v, ok := f.optionMeta["file_perm"].(uint32); ok { 128 | perm = v 129 | } 130 | f, e := os.OpenFile(val, mode, os.FileMode(perm)) 131 | return reflect.ValueOf(f), e 132 | } 133 | panic("Invalid execution path") 134 | } 135 | 136 | func tcpAddrValueParser(f *Flag, val string) (reflect.Value, error) { 137 | addr, err := net.ResolveTCPAddr("tcp", val) 138 | return reflect.ValueOf(addr), err 139 | } 140 | 141 | func urlValueParser(f *Flag, val string) (reflect.Value, error) { 142 | url, err := url.Parse(val) 143 | return reflect.ValueOf(url), err 144 | } 145 | 146 | func durationValueParser(f *Flag, val string) (reflect.Value, error) { 147 | d, err := time.ParseDuration(val) 148 | return reflect.ValueOf(d), err 149 | } 150 | 151 | func helpValueParser(f *Flag, val string) (reflect.Value, error) { 152 | return reflect.Value{}, ErrHelpRequest 153 | } 154 | -------------------------------------------------------------------------------- /valueparser_test.go: -------------------------------------------------------------------------------- 1 | package goptions 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestParse_File(t *testing.T) { 12 | var args []string 13 | var err error 14 | var fs *FlagSet 15 | var options struct { 16 | Output *os.File `goptions:"-o, create, trunc, wronly"` 17 | } 18 | 19 | args = []string{"-o", "testfile"} 20 | fs = NewFlagSet("goptions", &options) 21 | err = fs.Parse(args) 22 | if err != nil { 23 | t.Fatalf("Parsing failed: %s", err) 24 | } 25 | if !(options.Output != nil) { 26 | t.Fatalf("Unexpected value: %#v", options) 27 | } 28 | options.Output.Close() 29 | os.Remove("testfile") 30 | } 31 | 32 | func TestParse_TCPAddr(t *testing.T) { 33 | var args []string 34 | var err error 35 | var fs *FlagSet 36 | var options struct { 37 | Server *net.TCPAddr `goptions:"-a"` 38 | } 39 | 40 | args = []string{"-a", "192.168.0.100:8080"} 41 | fs = NewFlagSet("goptions", &options) 42 | err = fs.Parse(args) 43 | if err != nil { 44 | t.Fatalf("Parsing failed: %s", err) 45 | } 46 | if !(options.Server.IP.String() == "192.168.0.100" && 47 | options.Server.Port == 8080) { 48 | t.Fatalf("Unexpected value: %#v", options) 49 | } 50 | } 51 | 52 | func TestParse_URL(t *testing.T) { 53 | var args []string 54 | var err error 55 | var fs *FlagSet 56 | var options struct { 57 | Server *url.URL `goptions:"-a"` 58 | } 59 | 60 | args = []string{"-a", "http://www.google.com"} 61 | fs = NewFlagSet("goptions", &options) 62 | err = fs.Parse(args) 63 | if err != nil { 64 | t.Fatalf("Parsing failed: %s", err) 65 | } 66 | if !(options.Server.Scheme == "http" && 67 | options.Server.Host == "www.google.com") { 68 | t.Fatalf("Unexpected value: %#v", options.Server) 69 | } 70 | } 71 | 72 | func TestParse_Duration(t *testing.T) { 73 | var args []string 74 | var err error 75 | var fs *FlagSet 76 | var options struct { 77 | Cache time.Duration `goptions:"-d"` 78 | } 79 | 80 | args = []string{"-d", "1h45m"} 81 | fs = NewFlagSet("goptions", &options) 82 | err = fs.Parse(args) 83 | if err != nil { 84 | t.Fatalf("Parsing failed: %s", err) 85 | } 86 | if !(int64(options.Cache) != (1*60+45)*60*1e12) { 87 | t.Fatalf("Unexpected value: %#v", options.Cache) 88 | } 89 | } 90 | --------------------------------------------------------------------------------