├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── command.go ├── command_test.go ├── doc.go ├── example_basic_test.go ├── example_convenience_test.go ├── example_explicit_test.go ├── example_subcommand_test.go ├── help.go ├── help_test.go ├── option.go ├── option_test.go ├── template.go ├── template_legacy.go └── version.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - 1.2.2 6 | - 1.3.3 7 | - 1.4.3 8 | - 1.5.3 9 | - tip 10 | 11 | matrix: 12 | allow_failures: 13 | - go: tip 14 | 15 | script: 16 | - go tool -n vet || go get golang.org/x/tools/cmd/vet 17 | - go vet 18 | - go test -v 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Writ Changelog 2 | 3 | ## 0.8.9 (2016-02-11) 4 | - Fix: The error message for repeated aggregate short-form options reported the full aggregate (-hh) 5 | - Fix: The error message for repeated options always referenced args[0] rather than the current arg 6 | 7 | ## 0.8.8 (2016-02-06) 8 | - Misc: Fix misc typos 9 | - Misc: Fix description inconsistency in Greeter example 10 | - Misc: Rename method receivers in subcommand example 11 | - Misc: Clarify OptionGroup usage for explicit example 12 | 13 | ## 0.8.7 (2016-02-04) 14 | - Misc: Update references for renamed GitHub account 15 | 16 | ## 0.8.6 (2016-01-27) 17 | - Fix: Update exported field check for Go 1.6 18 | - Docs: Misc updates and clarifications 19 | 20 | ## 0.8.5 (2016-01-24) 21 | 22 | - Fix: Add a missing nil check to NewOptionDecoder 23 | - Fix: Fix wrapping for multi-line descriptions 24 | - Tests: Add coverage for remaining code, except Command.ExitHelp(). Coverage is at 98.7%. 25 | - Docs: Overhaul docs and examples for brevity 26 | - Docs: Add an example for subcommand handling 27 | 28 | ## 0.8.4 (2016-01-22) 29 | 30 | - Feature: Hide options and commands with empty descriptions from help output 31 | 32 | ## 0.8.3 (2016-01-22) 33 | 34 | - Misc: Minor code cleanup 35 | - Tests: Add basic test coverage for help output 36 | - Tests: Add additional test coverage for comamnds and options 37 | 38 | ## 0.8.2 (2016-01-22) 39 | 40 | - Fix: Stop parsing subcommands after a bare "-" argument 41 | - Fix: Ensure command and option names have no spaces in them 42 | - Tests: Add additional test coverage for comamnds and options 43 | 44 | ## 0.8.1 (2016-01-22) 45 | 46 | - API: Panic NewOptionDecoder() if input type is unsupported 47 | - Docs: Add an example of explicitly creating a Command and Options 48 | - Docs: Update documentation 49 | 50 | ## 0.8.0 (2016-01-22) 51 | 52 | - Misc: Initial release on Github 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Bob Ziuchkovski 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/bobziuchkovski/writ.svg?branch=master)](https://travis-ci.org/bobziuchkovski/writ) 2 | [![Coverage](https://gocover.io/_badge/github.com/bobziuchkovski/writ?1)](https://gocover.io/github.com/bobziuchkovski/writ) 3 | [![Report Card](http://goreportcard.com/badge/bobziuchkovski/writ)](http://goreportcard.com/report/bobziuchkovski/writ) 4 | [![GoDoc](https://godoc.org/github.com/bobziuchkovski/writ?status.svg)](https://godoc.org/github.com/bobziuchkovski/writ) 5 | 6 | # Writ 7 | 8 | ## Overview 9 | 10 | Writ is a flexible option parser with thorough test coverage. It's meant to be simple and "just work". Applications 11 | using writ look and behave similar to common GNU command-line applications, making them comfortable for end-users. 12 | 13 | Writ implements option decoding with GNU getopt_long conventions. All long and short-form option variations are 14 | supported: `--with-x`, `--name Sam`, `--day=Friday`, `-i FILE`, `-vvv`, etc. 15 | 16 | Help output generation is supported using text/template. The default template can be overriden with a custom template. 17 | 18 | ## API Promise 19 | 20 | Minor breaking changes may occur prior to the 1.0 release. After the 1.0 release, the API is guaranteed to remain backwards compatible. 21 | 22 | ## Basic Use 23 | 24 | Please see the [godocs](https://godoc.org/github.com/bobziuchkovski/writ) for additional information. 25 | 26 | This example uses writ.New() to build a command from the Greeter's struct fields. The resulting *writ.Command decodes 27 | and updates the Greeter's fields in-place. The Command.ExitHelp() method is used to display help content if --help is 28 | specified, or if invalid input arguments are received. 29 | 30 | Source: 31 | 32 | ```go 33 | package main 34 | 35 | import ( 36 | "fmt" 37 | "github.com/bobziuchkovski/writ" 38 | "strings" 39 | ) 40 | 41 | type Greeter struct { 42 | HelpFlag bool `flag:"help" description:"Display this help message and exit"` 43 | Verbosity int `flag:"v, verbose" description:"Display verbose output"` 44 | Name string `option:"n, name" default:"Everyone" description:"The person or people to greet"` 45 | } 46 | 47 | func main() { 48 | greeter := &Greeter{} 49 | cmd := writ.New("greeter", greeter) 50 | cmd.Help.Usage = "Usage: greeter [OPTION]... MESSAGE" 51 | cmd.Help.Header = "Greet users, displaying MESSAGE" 52 | 53 | // Use cmd.Decode(os.Args[1:]) in a real application 54 | _, positional, err := cmd.Decode([]string{"-vvv", "--name", "Sam", "How's it going?"}) 55 | if err != nil || greeter.HelpFlag { 56 | cmd.ExitHelp(err) 57 | } 58 | 59 | message := strings.Join(positional, " ") 60 | fmt.Printf("Hi %s! %s\n", greeter.Name, message) 61 | if greeter.Verbosity > 0 { 62 | fmt.Printf("I'm feeling re%slly chatty today!\n", strings.Repeat("a", greeter.Verbosity)) 63 | } 64 | 65 | // Output: 66 | // Hi Sam! How's it going? 67 | // I'm feeling reaaally chatty today! 68 | } 69 | ``` 70 | 71 | Help output: 72 | 73 | ``` 74 | Usage: greeter [OPTION]... MESSAGE 75 | Greet users, displaying MESSAGE 76 | 77 | Available Options: 78 | --help Display this help message and exit 79 | -v, --verbose Display verbose output 80 | -n, --name=ARG The person or people to greet 81 | ``` 82 | 83 | 84 | ### Subcommands 85 | 86 | Please see the [godocs](https://godoc.org/github.com/bobziuchkovski/writ) for additional information. 87 | 88 | This example demonstrates subcommands in a busybox style. There's no requirement that subcommands implement the Run() 89 | method shown here. It's just an example of how subcommands might be implemented. 90 | 91 | Source: 92 | 93 | ```go 94 | package main 95 | 96 | import ( 97 | "errors" 98 | "github.com/bobziuchkovski/writ" 99 | "os" 100 | ) 101 | 102 | type GoBox struct { 103 | Link Link `command:"ln" alias:"link" description:"Create a soft or hard link"` 104 | List List `command:"ls" alias:"list" description:"List directory contents"` 105 | } 106 | 107 | type Link struct { 108 | HelpFlag bool `flag:"h, help" description:"Display this message and exit"` 109 | Symlink bool `flag:"s" description:"Create a symlink instead of a hard link"` 110 | } 111 | 112 | type List struct { 113 | HelpFlag bool `flag:"h, help" description:"Display this message and exit"` 114 | LongFormat bool `flag:"l" description:"Use long-format output"` 115 | } 116 | 117 | func (g *GoBox) Run(p writ.Path, positional []string) { 118 | // The user didn't specify a subcommand. Give them help. 119 | p.Last().ExitHelp(errors.New("COMMAND is required")) 120 | } 121 | 122 | func (l *Link) Run(p writ.Path, positional []string) { 123 | if l.HelpFlag { 124 | p.Last().ExitHelp(nil) 125 | } 126 | if len(positional) != 2 { 127 | p.Last().ExitHelp(errors.New("ln requires two arguments, OLD and NEW")) 128 | } 129 | // Link operation omitted for brevity. This would be os.Link or os.Symlink 130 | // based on the l.Symlink value. 131 | } 132 | 133 | func (l *List) Run(p writ.Path, positional []string) { 134 | if l.HelpFlag { 135 | p.Last().ExitHelp(nil) 136 | } 137 | // Listing operation omitted for brevity. This would be a call to ioutil.ReadDir 138 | // followed by conditional formatting based on the l.LongFormat value. 139 | } 140 | 141 | func main() { 142 | gobox := &GoBox{} 143 | cmd := writ.New("gobox", gobox) 144 | cmd.Help.Usage = "Usage: gobox COMMAND [OPTION]... [ARG]..." 145 | cmd.Subcommand("ln").Help.Usage = "Usage: gobox ln [-s] OLD NEW" 146 | cmd.Subcommand("ls").Help.Usage = "Usage: gobox ls [-l] [PATH]..." 147 | 148 | path, positional, err := cmd.Decode(os.Args[1:]) 149 | if err != nil { 150 | // Using path.Last() here ensures the user sees relevant help for their 151 | // command selection 152 | path.Last().ExitHelp(err) 153 | } 154 | 155 | // At this point, cmd.Decode() has already decoded option values into the gobox 156 | // struct, including subcommand values. We just need to dispatch the command. 157 | // path.String() is guaranteed to represent the user command selection. 158 | switch path.String() { 159 | case "gobox": 160 | gobox.Run(path, positional) 161 | case "gobox ln": 162 | gobox.Link.Run(path, positional) 163 | case "gobox ls": 164 | gobox.List.Run(path, positional) 165 | default: 166 | panic("BUG: Someone added a new command and forgot to add it's path here") 167 | } 168 | } 169 | ``` 170 | 171 | Help output, gobox: 172 | 173 | ``` 174 | Usage: gobox COMMAND [OPTION]... [ARG]... 175 | 176 | Available Commands: 177 | ln Create a soft or hard link 178 | ls List directory contents 179 | ``` 180 | 181 | Help output, gobox ln: 182 | 183 | ``` 184 | Usage: gobox ln [-s] OLD NEW 185 | 186 | Available Options: 187 | -h, --help Display this message and exit 188 | -s Create a symlink instead of a hard link 189 | ``` 190 | 191 | Help output, gobox ls: 192 | 193 | ``` 194 | Usage: gobox ls [-l] [PATH]... 195 | 196 | Available Options: 197 | -h, --help Display this message and exit 198 | -l Use long-format output 199 | ``` 200 | 201 | 202 | 203 | ### Explicit Commands and Options 204 | 205 | Please see the [godocs](https://godoc.org/github.com/bobziuchkovski/writ) for additional information. 206 | 207 | This example demonstrates explicit Command and Option creation, along with explicit option grouping. 208 | It checks the host platform and dynamically adds a --bootloader option if the example is run on 209 | Linux. The same result could be achieved by using writ.New() to construct a Command, and then adding 210 | the platform-specific option to the resulting Command directly. 211 | 212 | Source: 213 | 214 | ```go 215 | package main 216 | 217 | import ( 218 | "github.com/bobziuchkovski/writ" 219 | "os" 220 | "runtime" 221 | ) 222 | 223 | type Config struct { 224 | help bool 225 | verbosity int 226 | bootloader string 227 | } 228 | 229 | func main() { 230 | config := &Config{} 231 | cmd := &writ.Command{Name: "explicit"} 232 | cmd.Help.Usage = "Usage: explicit [OPTION]... [ARG]..." 233 | cmd.Options = []*writ.Option{ 234 | { 235 | Names: []string{"h", "help"}, 236 | Description: "Display this help text and exit", 237 | Decoder: writ.NewFlagDecoder(&config.help), 238 | Flag: true, 239 | }, 240 | { 241 | Names: []string{"v"}, 242 | Description: "Increase verbosity; may be specified more than once", 243 | Decoder: writ.NewFlagAccumulator(&config.verbosity), 244 | Flag: true, 245 | Plural: true, 246 | }, 247 | } 248 | 249 | // Note the explicit option grouping. Using writ.New(), a single option group is 250 | // created for all options/flags that have descriptions. Without writ.New(), we 251 | // need to create the OptionGroup(s) ourselves. 252 | general := cmd.GroupOptions("help", "v") 253 | general.Header = "General Options:" 254 | cmd.Help.OptionGroups = append(cmd.Help.OptionGroups, general) 255 | 256 | // Dynamically add --bootloader on Linux 257 | if runtime.GOOS == "linux" { 258 | cmd.Options = append(cmd.Options, &writ.Option{ 259 | Names: []string{"bootloader"}, 260 | Description: "Use the specified bootloader (grub, grub2, or lilo)", 261 | Decoder: writ.NewOptionDecoder(&config.bootloader), 262 | Placeholder: "NAME", 263 | }) 264 | platform := cmd.GroupOptions("bootloader") 265 | platform.Header = "Platform Options:" 266 | cmd.Help.OptionGroups = append(cmd.Help.OptionGroups, platform) 267 | } 268 | 269 | // Decode the options 270 | _, _, err := cmd.Decode(os.Args[1:]) 271 | if err != nil || config.help { 272 | cmd.ExitHelp(err) 273 | } 274 | } 275 | ``` 276 | 277 | Help output, Linux: 278 | 279 | ``` 280 | General Options: 281 | -h, --help Display this help text and exit 282 | -v Increase verbosity; may be specified more than once 283 | 284 | Platform Options: 285 | --bootloader=NAME Use the specified bootloader (grub, grub2, or lilo) 286 | ``` 287 | 288 | Help output, other platforms: 289 | 290 | ``` 291 | General Options: 292 | -h, --help Display this help text and exit 293 | -v Increase verbosity; may be specified more than once 294 | ``` 295 | 296 | ## Authors 297 | 298 | Bob Ziuchkovski (@bobziuchkovski) 299 | 300 | ## License (MIT) 301 | 302 | Copyright (c) 2016 Bob Ziuchkovski 303 | 304 | Permission is hereby granted, free of charge, to any person obtaining a copy 305 | of this software and associated documentation files (the "Software"), to deal 306 | in the Software without restriction, including without limitation the rights 307 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 308 | copies of the Software, and to permit persons to whom the Software is 309 | furnished to do so, subject to the following conditions: 310 | 311 | The above copyright notice and this permission notice shall be included in 312 | all copies or substantial portions of the Software. 313 | 314 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 315 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 316 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 317 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 318 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 319 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 320 | THE SOFTWARE. 321 | 322 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package writ 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "io" 27 | "os" 28 | "reflect" 29 | "strings" 30 | "text/template" 31 | "unicode" 32 | ) 33 | 34 | type commandError struct { 35 | err error 36 | } 37 | 38 | func (e commandError) Error() string { 39 | return e.err.Error() 40 | } 41 | 42 | // panicCommand reports invalid use of the Command type 43 | func panicCommand(format string, values ...interface{}) { 44 | e := commandError{fmt.Errorf(format, values...)} 45 | panic(e) 46 | } 47 | 48 | // Path represents a parsed Command list as returned by Command.Decode(). 49 | // It is used to differentiate between user selection of commands and 50 | // subcommands. 51 | type Path []*Command 52 | 53 | // String returns the names of each command joined by spaces. 54 | func (p Path) String() string { 55 | var parts []string 56 | for _, cmd := range p { 57 | parts = append(parts, cmd.Name) 58 | } 59 | return strings.Join(parts, " ") 60 | } 61 | 62 | // First returns the first command of the path. This is the top-level/root command 63 | // where Decode() was invoked. 64 | func (p Path) First() *Command { 65 | return p[0] 66 | } 67 | 68 | // Last returns the last command of the path. This is the user-selected command. 69 | func (p Path) Last() *Command { 70 | return p[len(p)-1] 71 | } 72 | 73 | // findOption searches for the named option on the nearest ancestor command 74 | func (p Path) findOption(name string) *Option { 75 | for i := len(p) - 1; i >= 0; i-- { 76 | o := p[i].Option(name) 77 | if o != nil { 78 | return o 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | // New reads the input spec, searching for fields tagged with "option", 85 | // "flag", or "command". The field type and tags are used to construct 86 | // a corresponding Command instance, which can be used to decode program 87 | // arguments. See the package overview documentation for details. 88 | // 89 | // NOTE: The spec value must be a pointer to a struct. 90 | func New(name string, spec interface{}) *Command { 91 | cmd := parseCommandSpec(name, spec, nil) 92 | cmd.validate() 93 | return cmd 94 | } 95 | 96 | // Command specifies program options and subcommands. 97 | // 98 | // NOTE: If building a *Command directly without New(), the Help output 99 | // will be empty by default. Most applications will want to set the 100 | // Help.Usage and Help.CommandGroups / Help.OptionGroups fields as 101 | // appropriate. 102 | type Command struct { 103 | // Required 104 | Name string 105 | 106 | // Optional 107 | Aliases []string 108 | Options []*Option 109 | Subcommands []*Command 110 | Help Help 111 | Description string // Commands without descriptions are hidden 112 | } 113 | 114 | // String returns the command's name. 115 | func (c *Command) String() string { 116 | return c.Name 117 | } 118 | 119 | // Decode parses the given arguments according to GNU getopt_long conventions. 120 | // It matches Option arguments, both short and long-form, and decodes those 121 | // arguments with the matched Option's Decoder field. If the Command has 122 | // associated subcommands, the subcommand names are matched and extracted 123 | // from the start of the positional arguments. 124 | // 125 | // To avoid ambiguity, subcommand matching terminates at the first unmatched 126 | // positional argument. Similarly, option names are matched against the 127 | // command hierarchy as it exists at the point the option is encountered. If 128 | // command "first" has a subcommand "second", and "second" has an option 129 | // "foo", then "first second --foo" is valid but "first --foo second" returns 130 | // an error. If the two commands, "first" and "second", both specify a "bar" 131 | // option, then "first --bar second" decodes "bar" on "first", whereas 132 | // "first second --bar" decodes "bar" on "second". 133 | // 134 | // As with GNU getopt_long, a bare "--" argument terminates argument parsing. 135 | // All arguments after the first "--" argument are considered positional 136 | // parameters. 137 | func (c *Command) Decode(args []string) (path Path, positional []string, err error) { 138 | c.validate() 139 | c.setDefaults() 140 | return parseArgs(c, args) 141 | } 142 | 143 | // Subcommand locates subcommands on the method receiver. It returns a match 144 | // if any of the receiver's subcommands have a matching name or alias. Otherwise 145 | // it returns nil. 146 | func (c *Command) Subcommand(name string) *Command { 147 | for _, sub := range c.Subcommands { 148 | if sub.Name == name { 149 | return sub 150 | } 151 | for _, a := range sub.Aliases { 152 | if a == name { 153 | return sub 154 | } 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | // Option locates options on the method receiver. It returns a match if any of 161 | // the receiver's options have a matching name. Otherwise it returns nil. Options 162 | // are searched only on the method receiver, not any of it's subcommands. 163 | func (c *Command) Option(name string) *Option { 164 | for _, o := range c.Options { 165 | for _, n := range o.Names { 166 | if name == n { 167 | return o 168 | } 169 | } 170 | } 171 | return nil 172 | } 173 | 174 | // GroupOptions is used to build OptionGroups for help output. It searches the 175 | // method receiver for the named options and returns a corresponding OptionGroup. 176 | // If any of the named options are not found, GroupOptions panics. 177 | func (c *Command) GroupOptions(names ...string) OptionGroup { 178 | var group OptionGroup 179 | for _, n := range names { 180 | o := c.Option(n) 181 | if o == nil { 182 | panicCommand("Option not found: %s", n) 183 | } 184 | group.Options = append(group.Options, o) 185 | } 186 | return group 187 | } 188 | 189 | // GroupCommands is used to build CommandGroups for help output. It searches the 190 | // method receiver for the named subcommands and returns a corresponding CommandGroup. 191 | // If any of the named subcommands are not found, GroupCommands panics. 192 | func (c *Command) GroupCommands(names ...string) CommandGroup { 193 | var group CommandGroup 194 | for _, n := range names { 195 | c := c.Subcommand(n) 196 | if c == nil { 197 | panicCommand("Option not found: %s", n) 198 | } 199 | group.Commands = append(group.Commands, c) 200 | } 201 | return group 202 | } 203 | 204 | // WriteHelp renders help output to the given io.Writer. Output is influenced 205 | // by the Command's Help field. See the Help type for details. 206 | func (c *Command) WriteHelp(w io.Writer) error { 207 | var tmpl *template.Template 208 | if c.Help.Template != nil { 209 | tmpl = c.Help.Template 210 | } else { 211 | tmpl = defaultTemplate 212 | } 213 | 214 | buf := bytes.NewBuffer(nil) 215 | err := tmpl.Execute(buf, c) 216 | if err != nil { 217 | panicCommand("failed to render help: %s", err) 218 | } 219 | _, err = buf.WriteTo(w) 220 | return err 221 | } 222 | 223 | // ExitHelp writes help output and terminates the program. If err is nil, 224 | // the output is written to os.Stdout and the program terminates with a 0 exit 225 | // code. Otherwise, both the help output and error message are written to 226 | // os.Stderr and the program terminates with a 1 exit code. 227 | func (c *Command) ExitHelp(err error) { 228 | if err == nil { 229 | c.WriteHelp(os.Stdout) 230 | os.Exit(0) 231 | } 232 | c.WriteHelp(os.Stderr) 233 | fmt.Fprintf(os.Stderr, "\nError: %s\n", err) 234 | os.Exit(1) 235 | } 236 | 237 | // validate command spec 238 | func (c *Command) validate() { 239 | if c.Name == "" { 240 | panicCommand("Command name cannot be empty") 241 | } 242 | if strings.HasPrefix(c.Name, "-") { 243 | panicCommand("Command names cannot begin with '-' (command %s)", c.Name) 244 | } 245 | runes := []rune(c.Name) 246 | for _, r := range runes { 247 | if unicode.IsSpace(r) { 248 | panicCommand("Command names cannot have spaces (command %q)", c.Name) 249 | } 250 | } 251 | 252 | for _, a := range c.Aliases { 253 | if strings.HasPrefix(a, "-") { 254 | panicCommand("Command aliases cannot begin with '-' (command %s, alias %s)", c.Name, a) 255 | } 256 | runes := []rune(a) 257 | for _, r := range runes { 258 | if unicode.IsSpace(r) { 259 | panicCommand("Command aliases cannot have spaces (command %s, alias %q)", c.Name, a) 260 | } 261 | } 262 | } 263 | 264 | seen := make(map[string]bool) 265 | for _, sub := range c.Subcommands { 266 | sub.validate() 267 | subnames := append(sub.Aliases, sub.Name) 268 | for _, name := range subnames { 269 | _, present := seen[name] 270 | if present { 271 | panicCommand("command names must be unique (%s is specified multiple times)", name) 272 | } 273 | seen[name] = true 274 | } 275 | } 276 | 277 | seen = make(map[string]bool) 278 | for _, o := range c.Options { 279 | o.validate() 280 | for _, name := range o.Names { 281 | _, present := seen[name] 282 | if present { 283 | panicCommand("option names must be unique (%s is specified multiple times)", name) 284 | } 285 | seen[name] = true 286 | } 287 | } 288 | } 289 | 290 | func (c *Command) setDefaults() { 291 | for _, opt := range c.Options { 292 | defaulter, ok := opt.Decoder.(OptionDefaulter) 293 | if ok { 294 | defaulter.SetDefault() 295 | } 296 | } 297 | for _, sub := range c.Subcommands { 298 | sub.setDefaults() 299 | } 300 | } 301 | 302 | /* 303 | * Argument parsing 304 | */ 305 | 306 | func parseArgs(c *Command, args []string) (path Path, positional []string, err error) { 307 | path = Path{c} 308 | positional = make([]string, 0) // positional args should never be nil 309 | 310 | seen := make(map[*Option]bool) 311 | parseCmd, parseOpt := true, true 312 | for i := 0; i < len(args); i++ { 313 | a := args[i] 314 | if parseCmd { 315 | subcmd := path.Last().Subcommand(a) 316 | if subcmd != nil { 317 | path = append(path, subcmd) 318 | continue 319 | } 320 | } 321 | 322 | if parseOpt && strings.HasPrefix(a, "-") { 323 | if a == "-" { 324 | positional = append(positional, a) 325 | parseCmd = false 326 | continue 327 | } 328 | if a == "--" { 329 | parseOpt = false 330 | parseCmd = false 331 | continue 332 | } 333 | 334 | var opt *Option 335 | opt, args, err = processOption(path, args, i) 336 | if err != nil { 337 | return 338 | } 339 | _, present := seen[opt] 340 | if present && !opt.Plural { 341 | err = fmt.Errorf("option %q specified too many times", args[i]) 342 | return 343 | } 344 | seen[opt] = true 345 | continue 346 | } 347 | 348 | // Unmatched positional arg 349 | parseCmd = false 350 | positional = append(positional, a) 351 | } 352 | return 353 | } 354 | 355 | func processOption(path Path, args []string, optidx int) (opt *Option, newargs []string, err error) { 356 | if strings.HasPrefix(args[optidx], "--") { 357 | return processLongOption(path, args, optidx) 358 | } 359 | return processShortOption(path, args, optidx) 360 | } 361 | 362 | func processLongOption(path Path, args []string, optidx int) (opt *Option, newargs []string, err error) { 363 | keyval := strings.SplitN(strings.TrimPrefix(args[optidx], "--"), "=", 2) 364 | name := keyval[0] 365 | newargs = args 366 | 367 | opt = path.findOption(name) 368 | if opt == nil { 369 | err = fmt.Errorf("option '--%s' is not recognized", name) 370 | return 371 | } 372 | if opt.Flag { 373 | if len(keyval) == 2 { 374 | err = fmt.Errorf("flag '--%s' does not accept an argument", name) 375 | } else { 376 | err = opt.Decoder.Decode("") 377 | } 378 | } else { 379 | if len(keyval) == 2 { 380 | err = opt.Decoder.Decode(keyval[1]) 381 | } else { 382 | if len(args[optidx:]) < 2 { 383 | err = fmt.Errorf("option '--%s' requires an argument", name) 384 | } else { 385 | // Consume the next arg 386 | err = opt.Decoder.Decode(args[optidx+1]) 387 | newargs = duplicateArgs(args) 388 | newargs = append(newargs[:optidx+1], newargs[optidx+2:]...) 389 | } 390 | } 391 | } 392 | return 393 | } 394 | 395 | func processShortOption(path Path, args []string, optidx int) (opt *Option, newargs []string, err error) { 396 | keyval := strings.SplitN(strings.TrimPrefix(args[optidx], "-"), "", 2) 397 | name := keyval[0] 398 | newargs = args 399 | 400 | opt = path.findOption(name) 401 | if opt == nil { 402 | err = fmt.Errorf("option '-%s' is not recognized", name) 403 | return 404 | } 405 | if opt.Flag { 406 | err = opt.Decoder.Decode("") 407 | if len(keyval) == 2 { 408 | // Short-form options are aggregated. TODO: Cleanup 409 | // Rewrite current arg as - and append remaining aggregate opts as a new arg after the current one 410 | newargs = duplicateArgs(args) 411 | newargs = append(newargs[:optidx+1], append([]string{"-" + keyval[1]}, newargs[optidx+1:]...)...) 412 | newargs[optidx] = "-" + name 413 | } 414 | } else { 415 | if len(keyval) == 2 { 416 | err = opt.Decoder.Decode(keyval[1]) 417 | } else { 418 | if len(args[optidx:]) < 2 { 419 | err = fmt.Errorf("option '-%s' requires an argument", name) 420 | } else { 421 | // Consume the next arg 422 | err = opt.Decoder.Decode(args[optidx+1]) 423 | newargs = duplicateArgs(args) 424 | newargs = append(newargs[:optidx+1], newargs[optidx+2:]...) 425 | } 426 | } 427 | } 428 | return 429 | } 430 | 431 | func duplicateArgs(args []string) []string { 432 | dupe := make([]string, len(args)) 433 | for i := range args { 434 | dupe[i] = args[i] 435 | } 436 | return dupe 437 | } 438 | 439 | /* 440 | * Command spec parsing 441 | */ 442 | 443 | var ( 444 | decoderPtr *OptionDecoder 445 | decoderT = reflect.TypeOf(decoderPtr).Elem() 446 | 447 | aliasTag = "alias" 448 | commandTag = "command" 449 | defaultTag = "default" 450 | descriptionTag = "description" 451 | envTag = "env" 452 | flagTag = "flag" 453 | optionTag = "option" 454 | placeholderTag = "placeholder" 455 | invalidTags = map[string][]string{ 456 | commandTag: {defaultTag, envTag, flagTag, optionTag, placeholderTag}, 457 | flagTag: {aliasTag, commandTag, defaultTag, envTag, optionTag, placeholderTag}, 458 | optionTag: {aliasTag, commandTag, flagTag}, 459 | } 460 | ) 461 | 462 | func parseCommandSpec(name string, spec interface{}, path Path) *Command { 463 | rval := reflect.ValueOf(spec) 464 | if rval.Kind() != reflect.Ptr { 465 | panicCommand("command spec must be a pointer to struct type, not %s", rval.Kind()) 466 | } 467 | if rval.Elem().Kind() != reflect.Struct { 468 | panicCommand("command spec must be a pointer to struct type, not %s", rval.Kind()) 469 | } 470 | rval = rval.Elem() 471 | 472 | cmd := &Command{Name: name} 473 | path = append(path, cmd) 474 | 475 | for i := 0; i < rval.Type().NumField(); i++ { 476 | field := rval.Type().Field(i) 477 | fieldVal := rval.FieldByIndex(field.Index) 478 | if field.Tag.Get(commandTag) != "" { 479 | cmd.Subcommands = append(cmd.Subcommands, parseCommandField(field, fieldVal, path)) 480 | continue 481 | } 482 | if field.Tag.Get(flagTag) != "" { 483 | cmd.Options = append(cmd.Options, parseFlagField(field, fieldVal)) 484 | continue 485 | } 486 | if field.Tag.Get(optionTag) != "" { 487 | cmd.Options = append(cmd.Options, parseOptionField(field, fieldVal)) 488 | continue 489 | } 490 | } 491 | 492 | var visibleOpts []*Option 493 | for _, opt := range cmd.Options { 494 | if opt.Description != "" { 495 | visibleOpts = append(visibleOpts, opt) 496 | } 497 | } 498 | if len(visibleOpts) > 0 { 499 | cmd.Help.OptionGroups = []OptionGroup{ 500 | {Options: visibleOpts, Header: "Available Options:"}, 501 | } 502 | } 503 | var visibleSubs []*Command 504 | for _, sub := range cmd.Subcommands { 505 | if sub.Description != "" { 506 | visibleSubs = append(visibleSubs, sub) 507 | } 508 | } 509 | if len(visibleSubs) > 0 { 510 | cmd.Help.CommandGroups = []CommandGroup{ 511 | {Commands: visibleSubs, Header: "Available Commands:"}, 512 | } 513 | } 514 | cmd.Help.Usage = fmt.Sprintf("Usage: %s [OPTION]... [ARG]...", path.String()) 515 | return cmd 516 | } 517 | 518 | func parseCommandField(field reflect.StructField, fieldVal reflect.Value, path Path) *Command { 519 | checkTags(field, commandTag) 520 | checkExported(field, commandTag) 521 | 522 | names := parseCommaNames(field.Tag.Get(commandTag)) 523 | if len(names) == 0 { 524 | panicCommand("commands must have a name (field %s)", field.Name) 525 | } 526 | if len(names) != 1 { 527 | panicCommand("commands must have a single name (field %s)", field.Name) 528 | } 529 | 530 | cmd := parseCommandSpec(names[0], fieldVal.Addr().Interface(), path) 531 | cmd.Aliases = parseCommaNames(field.Tag.Get(aliasTag)) 532 | cmd.Description = field.Tag.Get(descriptionTag) 533 | cmd.validate() 534 | return cmd 535 | } 536 | 537 | func parseFlagField(field reflect.StructField, fieldVal reflect.Value) *Option { 538 | checkTags(field, flagTag) 539 | checkExported(field, flagTag) 540 | 541 | names := parseCommaNames(field.Tag.Get(flagTag)) 542 | if len(names) == 0 { 543 | panicCommand("at least one flag name must be specified (field %s)", field.Name) 544 | } 545 | 546 | opt := &Option{ 547 | Names: names, 548 | Flag: true, 549 | Description: field.Tag.Get(descriptionTag), 550 | } 551 | 552 | if field.Type.Implements(decoderT) { 553 | opt.Decoder = fieldVal.Interface().(OptionDecoder) 554 | } else if fieldVal.CanAddr() && reflect.PtrTo(field.Type).Implements(decoderT) { 555 | opt.Decoder = fieldVal.Addr().Interface().(OptionDecoder) 556 | } else { 557 | switch field.Type.Kind() { 558 | case reflect.Bool: 559 | opt.Decoder = NewFlagDecoder(fieldVal.Addr().Interface().(*bool)) 560 | case reflect.Int: 561 | opt.Decoder = NewFlagAccumulator(fieldVal.Addr().Interface().(*int)) 562 | opt.Plural = true 563 | default: 564 | panicCommand("field type not valid as a flag -- did you mean to use %q instead? (field %s)", "option", field.Name) 565 | } 566 | } 567 | 568 | opt.validate() 569 | return opt 570 | } 571 | 572 | func parseOptionField(field reflect.StructField, fieldVal reflect.Value) *Option { 573 | checkTags(field, optionTag) 574 | checkExported(field, optionTag) 575 | 576 | names := parseCommaNames(field.Tag.Get(optionTag)) 577 | if len(names) == 0 { 578 | panicCommand("at least one option name must be specified (field %s)", field.Name) 579 | } 580 | 581 | opt := &Option{ 582 | Names: names, 583 | Description: field.Tag.Get(descriptionTag), 584 | Placeholder: field.Tag.Get(placeholderTag), 585 | } 586 | 587 | if field.Type.Implements(decoderT) { 588 | opt.Decoder = fieldVal.Interface().(OptionDecoder) 589 | } else if fieldVal.CanAddr() && reflect.PtrTo(field.Type).Implements(decoderT) { 590 | opt.Decoder = fieldVal.Addr().Interface().(OptionDecoder) 591 | } else { 592 | if fieldVal.Kind() == reflect.Bool { 593 | panicCommand("bool fields are not valid as options. Use a %q tag instead (field %s)", "flag", field.Name) 594 | } 595 | if fieldVal.Kind() == reflect.Slice || fieldVal.Kind() == reflect.Map { 596 | opt.Plural = true 597 | } 598 | opt.Decoder = NewOptionDecoder(fieldVal.Addr().Interface()) 599 | } 600 | 601 | defaultArg := field.Tag.Get(defaultTag) 602 | if defaultArg != "" { 603 | opt.Decoder = NewDefaulter(opt.Decoder, defaultArg) 604 | } 605 | envName := field.Tag.Get(envTag) 606 | if envName != "" { 607 | opt.Decoder = NewEnvDefaulter(opt.Decoder, envName) 608 | } 609 | 610 | opt.validate() 611 | return opt 612 | } 613 | 614 | func checkTags(field reflect.StructField, fieldType string) { 615 | badTags, present := invalidTags[fieldType] 616 | if !present { 617 | panic("BUG: fieldType not present in invalidTags map") 618 | } 619 | for _, t := range badTags { 620 | if field.Tag.Get(t) != "" { 621 | panicCommand("tag %s is not valid for %ss (field %s)", t, fieldType, field.Name) 622 | } 623 | } 624 | } 625 | 626 | func checkExported(field reflect.StructField, fieldType string) { 627 | if field.PkgPath != "" && !field.Anonymous { 628 | panicCommand("%ss must be exported (field %s)", fieldType, field.Name) 629 | } 630 | } 631 | 632 | func parseCommaNames(spec string) []string { 633 | isSep := func(r rune) bool { 634 | return r == ',' || unicode.IsSpace(r) 635 | } 636 | return strings.FieldsFunc(spec, isSep) 637 | } 638 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package writ 22 | 23 | import ( 24 | "fmt" 25 | "io" 26 | "io/ioutil" 27 | "math" 28 | "os" 29 | "reflect" 30 | "strconv" 31 | "strings" 32 | "testing" 33 | ) 34 | 35 | func CompareField(structval interface{}, field string, value interface{}) (equal bool, fieldVal interface{}) { 36 | rval := reflect.ValueOf(structval) 37 | for rval.Kind() == reflect.Ptr { 38 | rval = rval.Elem() 39 | } 40 | f := rval.FieldByName(field) 41 | equal = reflect.DeepEqual(f.Interface(), value) 42 | fieldVal = f.Interface() 43 | return 44 | } 45 | 46 | /* 47 | * Test command and option routing for multi-tier commands 48 | */ 49 | 50 | type topSpec struct { 51 | MidSpec midSpec `command:"mid" alias:"second, 2nd" description:"a mid-level command"` 52 | HelpFlag bool `flag:"h, help" description:"help flag on a top-level command"` 53 | Top int `option:"t, topval" description:"an option on a top-level command"` 54 | } 55 | 56 | type midSpec struct { 57 | Mid int `option:"m, midval" description:"an option on a mid-level command"` 58 | HelpFlag bool `flag:"h, help" description:"help flag on a mid-level command"` 59 | BottomSpec bottomSpec `command:"bottom" alias:"third" description:"a bottom-level command"` 60 | } 61 | 62 | type bottomSpec struct { 63 | Bottom int `option:"b, bottomval" description:"an option on a bottom-level command"` 64 | HelpFlag bool `flag:"h, help" description:"help flag on a bottom-level command"` 65 | } 66 | 67 | type commandFieldTest struct { 68 | Args []string 69 | Valid bool 70 | Err string // TODO: rewrite all Valid: false cases with Err: message 71 | Path string 72 | Positional []string 73 | Field string 74 | Value interface{} 75 | SkipReason string 76 | } 77 | 78 | var commandFieldTests = []commandFieldTest{ 79 | // Path: top 80 | {Args: []string{}, Valid: true, Path: "top", Positional: []string{}}, 81 | {Args: []string{"-"}, Valid: true, Path: "top", Positional: []string{"-"}}, 82 | {Args: []string{"-", "mid"}, Valid: true, Path: "top", Positional: []string{"-", "mid"}}, 83 | {Args: []string{"--"}, Valid: true, Path: "top", Positional: []string{}}, 84 | {Args: []string{"--", "mid"}, Valid: true, Path: "top", Positional: []string{"mid"}}, 85 | {Args: []string{"-t", "1"}, Valid: true, Path: "top", Positional: []string{}, Field: "Top", Value: 1}, 86 | {Args: []string{"foo", "-t", "1"}, Valid: true, Path: "top", Positional: []string{"foo"}, Field: "Top", Value: 1}, 87 | {Args: []string{"-t", "1", "foo"}, Valid: true, Path: "top", Positional: []string{"foo"}, Field: "Top", Value: 1}, 88 | {Args: []string{"foo", "bar"}, Valid: true, Path: "top", Positional: []string{"foo", "bar"}}, 89 | {Args: []string{"foo", "-t", "1", "bar"}, Valid: true, Path: "top", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 90 | {Args: []string{"-t", "1", "foo", "bar"}, Valid: true, Path: "top", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 91 | {Args: []string{"--", "mid"}, Valid: true, Path: "top", Positional: []string{"mid"}}, 92 | {Args: []string{"-t", "1", "--", "mid"}, Valid: true, Path: "top", Positional: []string{"mid"}, Field: "Top", Value: 1}, 93 | {Args: []string{"-", "-t", "1", "--", "mid"}, Valid: true, Path: "top", Positional: []string{"-", "mid"}, Field: "Top", Value: 1}, 94 | {Args: []string{"--", "-t", "1", "mid"}, Valid: true, Path: "top", Positional: []string{"-t", "1", "mid"}, Field: "Top", Value: 0}, 95 | {Args: []string{"--", "-t", "1", "-", "mid"}, Valid: true, Path: "top", Positional: []string{"-t", "1", "-", "mid"}, Field: "Top", Value: 0}, 96 | {Args: []string{"bottom"}, Valid: true, Path: "top", Positional: []string{"bottom"}}, 97 | {Args: []string{"third"}, Valid: true, Path: "top", Positional: []string{"third"}}, 98 | {Args: []string{"bottom", "mid"}, Valid: true, Path: "top", Positional: []string{"bottom", "mid"}}, 99 | {Args: []string{"bottom", "second"}, Valid: true, Path: "top", Positional: []string{"bottom", "second"}}, 100 | {Args: []string{"bottom", "-", "second"}, Valid: true, Path: "top", Positional: []string{"bottom", "-", "second"}}, 101 | {Args: []string{"-m", "2"}, Valid: false}, 102 | {Args: []string{"--midval", "2"}, Valid: false}, 103 | {Args: []string{"-b", "3"}, Valid: false}, 104 | {Args: []string{"--bottomval", "3"}, Valid: false}, 105 | {Args: []string{"--bogus", "4"}, Valid: false}, 106 | {Args: []string{"--foo"}, Valid: false}, 107 | {Args: []string{"--foo=bar"}, Valid: false}, 108 | {Args: []string{"-f"}, Valid: false}, 109 | {Args: []string{"-f", "bar"}, Valid: false}, 110 | {Args: []string{"-fbar"}, Valid: false}, 111 | {Args: []string{"--help", "--help"}, Valid: false, Err: `option "--help" specified too many times`}, 112 | {Args: []string{"-h", "-h"}, Valid: false, Err: `option "-h" specified too many times`}, 113 | {Args: []string{"-hh"}, Valid: false, Err: `option "-h" specified too many times`}, 114 | 115 | // Path: top mid 116 | {Args: []string{"mid"}, Valid: true, Path: "top mid", Positional: []string{}}, 117 | {Args: []string{"mid", "-"}, Valid: true, Path: "top mid", Positional: []string{"-"}}, 118 | {Args: []string{"mid", "--"}, Valid: true, Path: "top mid", Positional: []string{}}, 119 | {Args: []string{"mid", "-", "bottom"}, Valid: true, Path: "top mid", Positional: []string{"-", "bottom"}}, 120 | {Args: []string{"mid", "--", "bottom"}, Valid: true, Path: "top mid", Positional: []string{"bottom"}}, 121 | {Args: []string{"mid", "-t", "1"}, Valid: true, Path: "top mid", Positional: []string{}, Field: "Top", Value: 1}, 122 | {Args: []string{"mid", "-", "-t", "1"}, Valid: true, Path: "top mid", Positional: []string{"-"}, Field: "Top", Value: 1}, 123 | {Args: []string{"mid", "--", "-t", "1"}, Valid: true, Path: "top mid", Positional: []string{"-t", "1"}, Field: "Top", Value: 0}, 124 | {Args: []string{"-t", "1", "mid"}, Valid: true, Path: "top mid", Positional: []string{}, Field: "Top", Value: 1}, 125 | {Args: []string{"mid", "foo", "-t", "1"}, Valid: true, Path: "top mid", Positional: []string{"foo"}, Field: "Top", Value: 1}, 126 | {Args: []string{"-t", "1", "mid", "foo"}, Valid: true, Path: "top mid", Positional: []string{"foo"}, Field: "Top", Value: 1}, 127 | {Args: []string{"mid", "-t", "1", "foo"}, Valid: true, Path: "top mid", Positional: []string{"foo"}, Field: "Top", Value: 1}, 128 | {Args: []string{"-t", "1", "mid", "foo"}, Valid: true, Path: "top mid", Positional: []string{"foo"}, Field: "Top", Value: 1}, 129 | {Args: []string{"mid", "foo", "-m", "2"}, Valid: true, Path: "top mid", Positional: []string{"foo"}, Field: "Mid", Value: 2}, 130 | {Args: []string{"mid", "-m", "2", "foo"}, Valid: true, Path: "top mid", Positional: []string{"foo"}, Field: "Mid", Value: 2}, 131 | {Args: []string{"mid", "foo", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}}, 132 | {Args: []string{"second", "foo", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}}, 133 | {Args: []string{"2nd", "foo", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}}, 134 | {Args: []string{"mid", "foo", "-t", "1", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 135 | {Args: []string{"mid", "-t", "1", "foo", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 136 | {Args: []string{"-t", "1", "mid", "foo", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 137 | {Args: []string{"mid", "foo", "-m", "2", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Mid", Value: 2}, 138 | {Args: []string{"-t", "1", "second", "foo", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 139 | {Args: []string{"second", "foo", "-m", "2", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Mid", Value: 2}, 140 | {Args: []string{"-t", "1", "2nd", "foo", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 141 | {Args: []string{"2nd", "foo", "-m", "2", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Mid", Value: 2}, 142 | {Args: []string{"mid", "-m", "2", "foo", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Mid", Value: 2}, 143 | {Args: []string{"mid", "-m", "2", "foo", "-t", "1", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 144 | {Args: []string{"mid", "-m", "2", "foo", "-t", "1", "bar"}, Valid: true, Field: "Mid", Value: 2}, 145 | {Args: []string{"mid", "-t", "1", "foo", "-m", "2", "bar"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 146 | {Args: []string{"mid", "-t", "1", "foo", "-m", "2", "bar"}, Valid: true, Field: "Mid", Value: 2}, 147 | {Args: []string{"-t", "1", "mid", "foo", "bar", "-m", "2"}, Valid: true, Path: "top mid", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 148 | {Args: []string{"-t", "1", "mid", "foo", "bar", "-m", "2"}, Valid: true, Field: "Mid", Value: 2}, 149 | {Args: []string{"mid", "-m", "2", "--"}, Valid: true, Path: "top mid", Positional: []string{}, Field: "Mid", Value: 2}, 150 | {Args: []string{"mid", "--", "-m", "2"}, Valid: true, Path: "top mid", Positional: []string{"-m", "2"}, Field: "Mid", Value: 0}, 151 | {Args: []string{"mid", "--", "bottom", "-b", "3"}, Valid: true, Path: "top mid", Positional: []string{"bottom", "-b", "3"}}, 152 | {Args: []string{"mid", "--", "bottom", "-b", "3", "--"}, Valid: true, Path: "top mid", Positional: []string{"bottom", "-b", "3", "--"}}, 153 | {Args: []string{"mid", "--", "bottom", "--", "-b", "3"}, Valid: true, Path: "top mid", Positional: []string{"bottom", "--", "-b", "3"}}, 154 | {Args: []string{"-m", "2", "mid"}, Valid: false}, 155 | {Args: []string{"--midval", "2", "mid"}, Valid: false}, 156 | {Args: []string{"-m", "2", "mid", "foo"}, Valid: false}, 157 | {Args: []string{"-b", "3", "mid"}, Valid: false}, 158 | {Args: []string{"-b", "3", "mid", "foo"}, Valid: false}, 159 | {Args: []string{"mid", "-b", "3"}, Valid: false}, 160 | {Args: []string{"mid", "-b", "3", "foo"}, Valid: false}, 161 | {Args: []string{"mid", "--bogus", "4"}, Valid: false}, 162 | {Args: []string{"mid", "--foo"}, Valid: false}, 163 | {Args: []string{"mid", "--foo=bar"}, Valid: false}, 164 | {Args: []string{"mid", "-f"}, Valid: false}, 165 | {Args: []string{"mid", "-f", "bar"}, Valid: false}, 166 | {Args: []string{"mid", "-fbar"}, Valid: false}, 167 | {Args: []string{"mid", "--help", "--help"}, Valid: false, Err: `option "--help" specified too many times`}, 168 | {Args: []string{"mid", "-h", "-h"}, Valid: false, Err: `option "-h" specified too many times`}, 169 | {Args: []string{"mid", "-hh"}, Valid: false, Err: `option "-h" specified too many times`}, 170 | 171 | // Path: top mid bottom 172 | {Args: []string{"mid", "bottom"}, Valid: true, Path: "top mid bottom", Positional: []string{}}, 173 | {Args: []string{"mid", "bottom", "-"}, Valid: true, Path: "top mid bottom", Positional: []string{"-"}}, 174 | {Args: []string{"mid", "bottom", "--"}, Valid: true, Path: "top mid bottom", Positional: []string{}}, 175 | {Args: []string{"mid", "bottom", "-t", "1"}, Valid: true, Path: "top mid bottom", Positional: []string{}, Field: "Top", Value: 1}, 176 | {Args: []string{"mid", "-t", "1", "bottom"}, Valid: true, Path: "top mid bottom", Positional: []string{}, Field: "Top", Value: 1}, 177 | {Args: []string{"-t", "1", "mid", "bottom"}, Valid: true, Path: "top mid bottom", Positional: []string{}, Field: "Top", Value: 1}, 178 | {Args: []string{"mid", "bottom", "foo", "-t", "1"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo"}, Field: "Top", Value: 1}, 179 | {Args: []string{"mid", "-t", "1", "bottom", "foo"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo"}, Field: "Top", Value: 1}, 180 | {Args: []string{"-t", "1", "mid", "bottom", "foo"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo"}, Field: "Top", Value: 1}, 181 | {Args: []string{"mid", "bottom", "foo", "-b", "3"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo"}, Field: "Bottom", Value: 3}, 182 | {Args: []string{"mid", "bottom", "-b", "3", "foo"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo"}, Field: "Bottom", Value: 3}, 183 | {Args: []string{"mid", "bottom", "foo", "bar"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}}, 184 | {Args: []string{"mid", "third", "-b", "3", "foo"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo"}, Field: "Bottom", Value: 3}, 185 | {Args: []string{"2nd", "third", "foo", "bar"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}}, 186 | {Args: []string{"-t", "1", "mid", "bottom", "foo", "bar"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Top", Value: 1}, 187 | {Args: []string{"mid", "bottom", "foo", "-b", "3", "bar"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Bottom", Value: 3}, 188 | {Args: []string{"mid", "bottom", "-b", "3", "foo", "bar"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Bottom", Value: 3}, 189 | {Args: []string{"mid", "-t", "1", "bottom", "-b", "3", "foo", "bar"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Bottom", Value: 3}, 190 | {Args: []string{"mid", "-t", "1", "bottom", "-b", "3", "foo", "bar"}, Valid: true, Field: "Top", Value: 1}, 191 | {Args: []string{"mid", "-m", "2", "bottom", "foo", "-b", "3", "bar"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Bottom", Value: 3}, 192 | {Args: []string{"mid", "-m", "2", "bottom", "foo", "-b", "3", "bar"}, Valid: true, Field: "Mid", Value: 2}, 193 | {Args: []string{"-t", "1", "mid", "bottom", "foo", "bar", "-b", "3"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Bottom", Value: 3}, 194 | {Args: []string{"-t", "1", "mid", "bottom", "foo", "bar", "-b", "3"}, Valid: true, Field: "Top", Value: 1}, 195 | {Args: []string{"-t", "1", "2nd", "bottom", "foo", "bar", "-b", "3"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Bottom", Value: 3}, 196 | {Args: []string{"-t", "1", "2nd", "bottom", "foo", "bar", "-b", "3"}, Valid: true, Field: "Top", Value: 1}, 197 | {Args: []string{"-t", "1", "mid", "third", "foo", "bar", "-b", "3"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Bottom", Value: 3}, 198 | {Args: []string{"-t", "1", "mid", "third", "foo", "bar", "-b", "3"}, Valid: true, Field: "Top", Value: 1}, 199 | {Args: []string{"-t", "1", "second", "third", "foo", "bar", "-b", "3"}, Valid: true, Path: "top mid bottom", Positional: []string{"foo", "bar"}, Field: "Bottom", Value: 3}, 200 | {Args: []string{"-t", "1", "second", "third", "foo", "bar", "-b", "3"}, Valid: true, Field: "Top", Value: 1}, 201 | {Args: []string{"mid", "bottom", "-b", "3", "--"}, Valid: true, Path: "top mid bottom", Positional: []string{}, Field: "Bottom", Value: 3}, 202 | {Args: []string{"mid", "bottom", "-", "-b", "3", "--"}, Valid: true, Path: "top mid bottom", Positional: []string{"-"}, Field: "Bottom", Value: 3}, 203 | {Args: []string{"mid", "bottom", "--", "-b", "3"}, Valid: true, Path: "top mid bottom", Positional: []string{"-b", "3"}, Field: "Bottom", Value: 0}, 204 | {Args: []string{"mid", "bottom", "-", "--", "-b", "3"}, Valid: true, Path: "top mid bottom", Positional: []string{"-", "-b", "3"}, Field: "Bottom", Value: 0}, 205 | {Args: []string{"mid", "-b", "3", "bottom"}, Valid: false}, 206 | {Args: []string{"bottom", "-b", "3"}, Valid: false}, 207 | {Args: []string{"-b", "3", "bottom"}, Valid: false}, 208 | {Args: []string{"-b", "3", "mid", "bottom"}, Valid: false}, 209 | {Args: []string{"mid", "--help", "--help", "bottom"}, Valid: false, Err: `option "--help" specified too many times`}, 210 | {Args: []string{"mid", "-h", "-h", "bottom"}, Valid: false, Err: `option "-h" specified too many times`}, 211 | {Args: []string{"mid", "-hh", "bottom"}, Valid: false, Err: `option "-h" specified too many times`}, 212 | 213 | // Duplicate option routing (HelpFlag) 214 | {Args: []string{"-h"}, Valid: true, Path: "top", Positional: []string{}, Field: "HelpFlagTop", Value: true}, 215 | {Args: []string{"-h"}, Valid: true, Field: "HelpFlagMid", Value: false}, 216 | {Args: []string{"-h"}, Valid: true, Field: "HelpFlagBottom", Value: false}, 217 | {Args: []string{"-h", "mid"}, Valid: true, Path: "top mid", Positional: []string{}, Field: "HelpFlagTop", Value: true}, 218 | {Args: []string{"-h", "mid"}, Valid: true, Field: "HelpFlagMid", Value: false}, 219 | {Args: []string{"-h", "mid"}, Valid: true, Field: "HelpFlagBottom", Value: false}, 220 | {Args: []string{"mid", "-h"}, Valid: true, Path: "top mid", Positional: []string{}, Field: "HelpFlagMid", Value: true}, 221 | {Args: []string{"mid", "-h"}, Valid: true, Field: "HelpFlagTop", Value: false}, 222 | {Args: []string{"mid", "-h"}, Valid: true, Field: "HelpFlagBottom", Value: false}, 223 | {Args: []string{"mid", "-h", "bottom"}, Valid: true, Path: "top mid bottom", Positional: []string{}, Field: "HelpFlagMid", Value: true}, 224 | {Args: []string{"mid", "-h", "bottom"}, Valid: true, Field: "HelpFlagTop", Value: false}, 225 | {Args: []string{"mid", "-h", "bottom"}, Valid: true, Field: "HelpFlagBottom", Value: false}, 226 | {Args: []string{"mid", "bottom", "-h"}, Valid: true, Path: "top mid bottom", Positional: []string{}, Field: "HelpFlagBottom", Value: true}, 227 | {Args: []string{"mid", "bottom", "-h"}, Valid: true, Field: "HelpFlagTop", Value: false}, 228 | {Args: []string{"mid", "bottom", "-h"}, Valid: true, Field: "HelpFlagMid", Value: false}, 229 | } 230 | 231 | func TestCommandFields(t *testing.T) { 232 | for _, test := range commandFieldTests { 233 | spec := &topSpec{} 234 | runCommandFieldTest(t, spec, test) 235 | } 236 | } 237 | 238 | func runCommandFieldTest(t *testing.T, spec *topSpec, test commandFieldTest) { 239 | if test.SkipReason != "" { 240 | t.Logf("Test skipped. Args: %q, Field: %s, Reason: %s", test.Args, test.Field, test.SkipReason) 241 | return 242 | } 243 | 244 | cmd := New("top", spec) 245 | path, positional, err := cmd.Decode(test.Args) 246 | values := map[string]interface{}{ 247 | "Top": spec.Top, 248 | "Mid": spec.MidSpec.Mid, 249 | "Bottom": spec.MidSpec.BottomSpec.Bottom, 250 | "HelpFlagTop": spec.HelpFlag, 251 | "HelpFlagMid": spec.MidSpec.HelpFlag, 252 | "HelpFlagBottom": spec.MidSpec.BottomSpec.HelpFlag, 253 | } 254 | if !test.Valid { 255 | if err == nil { 256 | t.Errorf("Expected error but none received. Args: %q", test.Args) 257 | } 258 | if test.Err != "" && err.Error() != test.Err { 259 | t.Errorf("Invalid error message. Expected: %s, Received: %s", test.Err, err.Error()) 260 | } 261 | return 262 | } 263 | if err != nil { 264 | t.Errorf("Received unexpected error. Field: %s, Args: %q, Error: %s", test.Field, test.Args, err) 265 | return 266 | } 267 | if test.Positional != nil && !reflect.DeepEqual(positional, test.Positional) { 268 | t.Errorf("Positional args are incorrect. Args: %q, Expected: %s, Received: %s", test.Args, test.Positional, positional) 269 | return 270 | } 271 | if test.Field != "" && !reflect.DeepEqual(values[test.Field], test.Value) { 272 | t.Errorf("Decoded value is incorrect. Field: %s, Args: %q, Expected: %#v, Received: %#v", test.Field, test.Args, test.Value, values[test.Field]) 273 | return 274 | } 275 | if path.First() != cmd { 276 | t.Errorf("Expected first command in path to be top-level command, but got %s instead.", path.First().Name) 277 | return 278 | } 279 | if test.Path != "" && path.String() != test.Path { 280 | t.Errorf("Command path is incorrect. Args: %q, Expected: %s, Received: %s", test.Args, test.Path, path) 281 | return 282 | } 283 | } 284 | 285 | func TestCommandString(t *testing.T) { 286 | cmd := New("top", &topSpec{}) 287 | if cmd.String() != "top" { 288 | t.Errorf("Invalid Command.String() value. Expected: %q, received: %q", "top", cmd.String()) 289 | } 290 | if cmd.Subcommand("mid").String() != "mid" { 291 | t.Errorf("Invalid Command.String() value. Expected: %q, received: %q", "mid", cmd.Subcommand("mid").String()) 292 | } 293 | if cmd.Subcommand("mid").Subcommand("bottom").String() != "bottom" { 294 | t.Errorf("Invalid Command.String() value. Expected: %q, received: %q", "bottom", cmd.Subcommand("mid").Subcommand("bottom").String()) 295 | } 296 | } 297 | 298 | /* 299 | * Test parsing of description metadata 300 | */ 301 | 302 | func TestSpecDescriptions(t *testing.T) { 303 | type Spec struct { 304 | Flag bool `flag:"flag" description:"a flag"` 305 | Option int `option:"option" description:"an option"` 306 | Command struct{} `command:"command" description:"a command"` 307 | } 308 | cmd := New("test", &Spec{}) 309 | if cmd.Option("flag").Description != "a flag" { 310 | t.Errorf("Flag description is incorrect. Expected: %q, Received: %q", "a flag", cmd.Option("flag").Description) 311 | } 312 | if cmd.Option("option").Description != "an option" { 313 | t.Errorf("Option description is incorrect. Expected: %q, Received: %q", "an option", cmd.Option("option").Description) 314 | } 315 | if cmd.Subcommand("command").Description != "a command" { 316 | t.Errorf("Command description is incorrect. Expected: %q, Received: %q", "a command", cmd.Subcommand("command").Description) 317 | } 318 | } 319 | 320 | /* 321 | * Test parsing of placeholder metadata 322 | */ 323 | 324 | func TestSpecPlaceholders(t *testing.T) { 325 | type Spec struct { 326 | Option int `option:"option" description:"an option" placeholder:"VALUE"` 327 | } 328 | cmd := New("test", &Spec{}) 329 | if cmd.Option("option").Placeholder != "VALUE" { 330 | t.Errorf("Option placeholder is incorrect. Expected: %q, Received: %q", "VALUE", cmd.Option("option").Placeholder) 331 | } 332 | } 333 | 334 | /* 335 | * Test default values on fields 336 | */ 337 | 338 | type defaultFieldSpec struct { 339 | Default int `option:"d" description:"An int field with a default" default:"42"` 340 | EnvDefault int `option:"e" description:"An int field with an environment default" env:"ENV_DEFAULT"` 341 | StackedDefault int `option:"s" description:"An int field with both a default and environment default" default:"84" env:"STACKED_DEFAULT"` 342 | } 343 | 344 | type defaultFieldTest struct { 345 | Args []string 346 | Valid bool 347 | Field string 348 | Value interface{} 349 | EnvKey string 350 | EnvValue string 351 | SkipReason string 352 | } 353 | 354 | var defaultFieldTests = []defaultFieldTest{ 355 | // Field with a default value 356 | {Args: []string{""}, Valid: true, Field: "Default", Value: 42}, 357 | {Args: []string{"-d", "2"}, Valid: true, Field: "Default", Value: 2}, 358 | {Args: []string{"-d", "foo"}, Valid: false}, 359 | 360 | // Field with an environment default 361 | {Args: []string{""}, Valid: true, Field: "EnvDefault", Value: 0}, 362 | {Args: []string{""}, Valid: true, EnvKey: "ENV_DEFAULT", EnvValue: "2", Field: "EnvDefault", Value: 2}, 363 | {Args: []string{""}, Valid: true, EnvKey: "ENV_DEFAULT", EnvValue: "foo", Field: "EnvDefault", Value: 0}, 364 | {Args: []string{"-e", "4"}, Valid: true, EnvKey: "ENV_DEFAULT", EnvValue: "2", Field: "EnvDefault", Value: 4}, 365 | {Args: []string{"-e", "4"}, Valid: true, EnvKey: "ENV_DEFAULT", EnvValue: "foo", Field: "EnvDefault", Value: 4}, 366 | {Args: []string{"-e", "foo"}, Valid: false, EnvKey: "ENV_DEFAULT", EnvValue: "2"}, 367 | 368 | // Field with both a default value and an environment default 369 | {Args: []string{""}, Valid: true, Field: "StackedDefault", Value: 84}, 370 | {Args: []string{""}, Valid: true, EnvKey: "STACKED_DEFAULT", EnvValue: "2", Field: "StackedDefault", Value: 2}, 371 | {Args: []string{""}, Valid: true, EnvKey: "STACKED_DEFAULT", EnvValue: "foo", Field: "StackedDefault", Value: 84}, 372 | {Args: []string{"-s", "4"}, Valid: true, EnvKey: "STACKED_DEFAULT", EnvValue: "2", Field: "StackedDefault", Value: 4}, 373 | {Args: []string{"-s", "4"}, Valid: true, EnvKey: "STACKED_DEFAULT", EnvValue: "foo", Field: "StackedDefault", Value: 4}, 374 | {Args: []string{"-s", "foo"}, Valid: false, EnvKey: "STACKED_DEFAULT", EnvValue: "foo"}, 375 | {Args: []string{"-s", "foo"}, Valid: false}, 376 | } 377 | 378 | func TestDefaultFields(t *testing.T) { 379 | for _, test := range defaultFieldTests { 380 | spec := &defaultFieldSpec{} 381 | runDefaultFieldTest(t, spec, test) 382 | } 383 | } 384 | 385 | func runDefaultFieldTest(t *testing.T, spec interface{}, test defaultFieldTest) { 386 | if test.SkipReason != "" { 387 | t.Logf("Test skipped. Args: %q, Field: %s, Reason: %s", test.Args, test.Field, test.SkipReason) 388 | return 389 | } 390 | 391 | if test.EnvKey != "" { 392 | realval := os.Getenv(test.EnvKey) 393 | defer (func() { os.Setenv(test.EnvKey, realval) })() 394 | os.Setenv(test.EnvKey, test.EnvValue) 395 | } 396 | cmd := New("test", spec) 397 | _, _, err := cmd.Decode(test.Args) 398 | 399 | if !test.Valid { 400 | if err == nil { 401 | t.Errorf("Expected error but none received. Args: %q", test.Args) 402 | } 403 | return 404 | } 405 | if err != nil { 406 | t.Errorf("Received unexpected error. Field: %s, Args: %q, Error: %s", test.Field, test.Args, err) 407 | return 408 | } 409 | equal, fieldval := CompareField(spec, test.Field, test.Value) 410 | if !equal { 411 | t.Errorf("Decoded value is incorrect. Field: %s, Args: %q, Expected: %#v, Received: %#v", test.Field, test.Args, test.Value, fieldval) 412 | return 413 | } 414 | } 415 | 416 | func TestBogusDefaultField(t *testing.T) { 417 | var spec = &struct { 418 | BogusDefault int `option:"b" description:"An int field with a bogus default" default:"bogus"` 419 | }{} 420 | 421 | defer func() { 422 | r := recover() 423 | if r != nil { 424 | switch r.(type) { 425 | case commandError, optionError: 426 | // Intentional No-op 427 | default: 428 | panic(r) 429 | } 430 | } 431 | }() 432 | 433 | cmd := New("test", spec) 434 | cmd.Decode([]string{}) 435 | t.Errorf("Expected decoding to panic on bogus default value, but this didn't happen.") 436 | } 437 | 438 | /* 439 | * Generic field test helpers 440 | */ 441 | 442 | type fieldTest struct { 443 | Args []string 444 | Valid bool 445 | Field string 446 | Value interface{} 447 | SkipReason string 448 | } 449 | 450 | func runFieldTest(t *testing.T, spec interface{}, test fieldTest) { 451 | if test.SkipReason != "" { 452 | t.Logf("Test skipped. Args: %q, Field: %s, Reason: %s", test.Args, test.Field, test.SkipReason) 453 | return 454 | } 455 | 456 | cmd := New("test", spec) 457 | _, _, err := cmd.Decode(test.Args) 458 | if !test.Valid { 459 | if err == nil { 460 | t.Errorf("Expected error but none received. Args: %q", test.Args) 461 | } 462 | return 463 | } 464 | if err != nil { 465 | t.Errorf("Received unexpected error. Field: %s, Args: %q, Error: %s", test.Field, test.Args, err) 466 | return 467 | } 468 | equal, fieldval := CompareField(spec, test.Field, test.Value) 469 | if !equal { 470 | t.Errorf("Decoded value is incorrect. Field: %s, Args: %q, Expected: %#v, Received: %#v", test.Field, test.Args, test.Value, fieldval) 471 | return 472 | } 473 | } 474 | 475 | /* 476 | * Test option name variations 477 | */ 478 | 479 | type optNameSpec struct { 480 | Bool bool `flag:" b, bool" description:"A bool flag"` 481 | Accumulator int `flag:"a,acc,A, accum" description:"An accumulator field"` 482 | Int int `option:"int, I" description:"An int field"` 483 | Float float32 `option:" float,F, FloaT, f " description:"A float field"` 484 | } 485 | 486 | var optNameTests = []fieldTest{ 487 | // Bool Flag 488 | {Args: []string{"-b"}, Valid: true, Field: "Bool", Value: true}, 489 | {Args: []string{"--bool"}, Valid: true, Field: "Bool", Value: true}, 490 | 491 | // Accumulator Flag 492 | {Args: []string{"-A", "--accum", "-a", "--acc", "-Aa", "-aA"}, Valid: true, Field: "Accumulator", Value: 8}, 493 | 494 | // Int Option 495 | {Args: []string{"-I2"}, Valid: true, Field: "Int", Value: 2}, 496 | {Args: []string{"-I", "2"}, Valid: true, Field: "Int", Value: 2}, 497 | {Args: []string{"--int", "2"}, Valid: true, Field: "Int", Value: 2}, 498 | {Args: []string{"--int=2"}, Valid: true, Field: "Int", Value: 2}, 499 | 500 | // Float Option 501 | {Args: []string{"-F2"}, Valid: true, Field: "Float", Value: float32(2.0)}, 502 | {Args: []string{"-F2.5"}, Valid: true, Field: "Float", Value: float32(2.5)}, 503 | {Args: []string{"-F", "2"}, Valid: true, Field: "Float", Value: float32(2.0)}, 504 | {Args: []string{"-F", "2.5"}, Valid: true, Field: "Float", Value: float32(2.5)}, 505 | {Args: []string{"-f2"}, Valid: true, Field: "Float", Value: float32(2.0)}, 506 | {Args: []string{"-f2.5"}, Valid: true, Field: "Float", Value: float32(2.5)}, 507 | {Args: []string{"-f", "2"}, Valid: true, Field: "Float", Value: float32(2.0)}, 508 | {Args: []string{"-f", "2.5"}, Valid: true, Field: "Float", Value: float32(2.5)}, 509 | {Args: []string{"--FloaT", "2"}, Valid: true, Field: "Float", Value: float32(2.0)}, 510 | {Args: []string{"--FloaT", "2.5"}, Valid: true, Field: "Float", Value: float32(2.5)}, 511 | {Args: []string{"--FloaT=2"}, Valid: true, Field: "Float", Value: float32(2.0)}, 512 | {Args: []string{"--FloaT=2.5"}, Valid: true, Field: "Float", Value: float32(2.5)}, 513 | {Args: []string{"--float", "2"}, Valid: true, Field: "Float", Value: float32(2.0)}, 514 | {Args: []string{"--float", "2.5"}, Valid: true, Field: "Float", Value: float32(2.5)}, 515 | {Args: []string{"--float=2"}, Valid: true, Field: "Float", Value: float32(2.0)}, 516 | {Args: []string{"--float=2.5"}, Valid: true, Field: "Float", Value: float32(2.5)}, 517 | } 518 | 519 | func TestOptionNames(t *testing.T) { 520 | for _, test := range optNameTests { 521 | spec := &optNameSpec{} 522 | runFieldTest(t, spec, test) 523 | } 524 | } 525 | 526 | /* 527 | * Test flag fields 528 | */ 529 | 530 | type flagFieldSpec struct { 531 | Bool bool `flag:"b, bool" description:"A bool flag"` 532 | Accumulator int `flag:"a, acc" description:"An accumulator flag"` 533 | } 534 | 535 | var flagTests = []fieldTest{ 536 | // Bool flag 537 | {Args: []string{}, Valid: true, Field: "Bool", Value: false}, 538 | {Args: []string{"-b"}, Valid: true, Field: "Bool", Value: true}, 539 | {Args: []string{"--bool"}, Valid: true, Field: "Bool", Value: true}, 540 | {Args: []string{"-b", "-b"}, Valid: false}, 541 | {Args: []string{"-b2"}, Valid: false}, 542 | {Args: []string{"--bool=2"}, Valid: false}, 543 | 544 | // Accumulator flag 545 | {Args: []string{}, Valid: true, Field: "Accumulator", Value: 0}, 546 | {Args: []string{"-a"}, Valid: true, Field: "Accumulator", Value: 1}, 547 | {Args: []string{"-a", "-a"}, Valid: true, Field: "Accumulator", Value: 2}, 548 | {Args: []string{"-aaa"}, Valid: true, Field: "Accumulator", Value: 3}, 549 | {Args: []string{"--acc", "-a"}, Valid: true, Field: "Accumulator", Value: 2}, 550 | {Args: []string{"-a", "--acc", "-aa"}, Valid: true, Field: "Accumulator", Value: 4}, 551 | {Args: []string{"-a3"}, Valid: false}, 552 | {Args: []string{"--acc=3"}, Valid: false}, 553 | } 554 | 555 | func TestFlagFields(t *testing.T) { 556 | for _, test := range flagTests { 557 | spec := &flagFieldSpec{} 558 | runFieldTest(t, spec, test) 559 | } 560 | } 561 | 562 | /* 563 | * Test map and slice field types 564 | */ 565 | 566 | type mapSliceFieldSpec struct { 567 | StringSlice []string `option:"s" description:"A string slice option" placeholder:"STRINGSLICE"` 568 | StringMap map[string]string `option:"m" description:"A map of strings option" placeholder:"KEY=VALUE"` 569 | } 570 | 571 | var mapSliceFieldTests = []fieldTest{ 572 | // String Slice 573 | {Args: []string{"-s", "1", "-s", "-1", "-s", "+1"}, Valid: true, Field: "StringSlice", Value: []string{"1", "-1", "+1"}}, 574 | {Args: []string{"-s", " a b", "-s", "\n", "-s", "\t"}, Valid: true, Field: "StringSlice", Value: []string{" a b", "\n", "\t"}}, 575 | {Args: []string{"-s", "日本", "-s", "-日本", "-s", "--日本"}, Valid: true, Field: "StringSlice", Value: []string{"日本", "-日本", "--日本"}}, 576 | {Args: []string{"-s", "1"}, Valid: true, Field: "StringSlice", Value: []string{"1"}}, 577 | {Args: []string{"-s", "-1"}, Valid: true, Field: "StringSlice", Value: []string{"-1"}}, 578 | {Args: []string{"-s", "+1"}, Valid: true, Field: "StringSlice", Value: []string{"+1"}}, 579 | {Args: []string{"-s", "1.0"}, Valid: true, Field: "StringSlice", Value: []string{"1.0"}}, 580 | {Args: []string{"-s", "0x01"}, Valid: true, Field: "StringSlice", Value: []string{"0x01"}}, 581 | {Args: []string{"-s", "-"}, Valid: true, Field: "StringSlice", Value: []string{"-"}}, 582 | {Args: []string{"-s", "-a"}, Valid: true, Field: "StringSlice", Value: []string{"-a"}}, 583 | {Args: []string{"-s", "--"}, Valid: true, Field: "StringSlice", Value: []string{"--"}}, 584 | {Args: []string{"-s", "--a"}, Valid: true, Field: "StringSlice", Value: []string{"--a"}}, 585 | {Args: []string{"-s", ""}, Valid: true, Field: "StringSlice", Value: []string{""}}, 586 | {Args: []string{"-s", " "}, Valid: true, Field: "StringSlice", Value: []string{" "}}, 587 | {Args: []string{"-s", " a"}, Valid: true, Field: "StringSlice", Value: []string{" a"}}, 588 | {Args: []string{"-s", "a "}, Valid: true, Field: "StringSlice", Value: []string{"a "}}, 589 | {Args: []string{"-s", "a b "}, Valid: true, Field: "StringSlice", Value: []string{"a b "}}, 590 | {Args: []string{"-s", " a b"}, Valid: true, Field: "StringSlice", Value: []string{" a b"}}, 591 | {Args: []string{"-s", "\n"}, Valid: true, Field: "StringSlice", Value: []string{"\n"}}, 592 | {Args: []string{"-s", "\t"}, Valid: true, Field: "StringSlice", Value: []string{"\t"}}, 593 | {Args: []string{"-s", "日本"}, Valid: true, Field: "StringSlice", Value: []string{"日本"}}, 594 | {Args: []string{"-s", "-日本"}, Valid: true, Field: "StringSlice", Value: []string{"-日本"}}, 595 | {Args: []string{"-s", "--日本"}, Valid: true, Field: "StringSlice", Value: []string{"--日本"}}, 596 | {Args: []string{"-s", " 日本"}, Valid: true, Field: "StringSlice", Value: []string{" 日本"}}, 597 | {Args: []string{"-s", "日本 "}, Valid: true, Field: "StringSlice", Value: []string{"日本 "}}, 598 | {Args: []string{"-s", "日 本"}, Valid: true, Field: "StringSlice", Value: []string{"日 本"}}, 599 | {Args: []string{"-s", "A relatively long string to make sure we aren't doing any silly truncation anywhere, since that would be bad..."}, Valid: true, Field: "StringSlice", Value: []string{"A relatively long string to make sure we aren't doing any silly truncation anywhere, since that would be bad..."}}, 600 | {Args: []string{"-s"}, Valid: false}, 601 | 602 | // String Map 603 | {Args: []string{"-m", "a=b"}, Valid: true, Field: "StringMap", Value: map[string]string{"a": "b"}}, 604 | {Args: []string{"-m", "a=b=c"}, Valid: true, Field: "StringMap", Value: map[string]string{"a": "b=c"}}, 605 | {Args: []string{"-m", "a=b "}, Valid: true, Field: "StringMap", Value: map[string]string{"a": "b "}}, 606 | {Args: []string{"-m", "a= b"}, Valid: true, Field: "StringMap", Value: map[string]string{"a": " b"}}, 607 | {Args: []string{"-m", "a =b"}, Valid: true, Field: "StringMap", Value: map[string]string{"a ": "b"}}, 608 | {Args: []string{"-m", " a=b"}, Valid: true, Field: "StringMap", Value: map[string]string{" a": "b"}}, 609 | {Args: []string{"-m", " a=b "}, Valid: true, Field: "StringMap", Value: map[string]string{" a": "b "}}, 610 | {Args: []string{"-m", "a = b "}, Valid: true, Field: "StringMap", Value: map[string]string{"a ": " b "}}, 611 | {Args: []string{"-m", " a = b "}, Valid: true, Field: "StringMap", Value: map[string]string{" a ": " b "}}, 612 | {Args: []string{"-m", "a=b", "-m", "a=c"}, Valid: true, Field: "StringMap", Value: map[string]string{"a": "c"}}, 613 | {Args: []string{"-m", "a=b", "-m", "c=d"}, Valid: true, Field: "StringMap", Value: map[string]string{"a": "b", "c": "d"}}, 614 | {Args: []string{"-m", "日=本", "-m", "-日=本", "-m", "--日=--本"}, Valid: true, Field: "StringMap", Value: map[string]string{"日": "本", "-日": "本", "--日": "--本"}}, 615 | {Args: []string{"-m", "a", "=b"}, Valid: false}, 616 | {Args: []string{"-m", "a", "b"}, Valid: false}, 617 | {Args: []string{"-m", "foo"}, Valid: false}, 618 | {Args: []string{"-m", "a:b"}, Valid: false}, 619 | {Args: []string{"-m"}, Valid: false}, 620 | } 621 | 622 | func TestMapSliceFields(t *testing.T) { 623 | for _, test := range mapSliceFieldTests { 624 | spec := &mapSliceFieldSpec{} 625 | runFieldTest(t, spec, test) 626 | } 627 | } 628 | 629 | /* 630 | * Test io field types 631 | */ 632 | 633 | const ioTestText = "test IO" 634 | 635 | type ioFieldSpec struct { 636 | Reader io.Reader `option:"reader" description:"An io.Reader input option"` 637 | ReadCloser io.ReadCloser `option:"readcloser" description:"An io.ReadCloser input option"` 638 | Writer io.Writer `option:"writer" description:"An io.Writer output option"` 639 | WriteCloser io.WriteCloser `option:"writecloser" description:"An io.WriteCloser output option"` 640 | } 641 | 642 | func (r *ioFieldSpec) PerformIO() error { 643 | input := []io.Reader{r.Reader, r.ReadCloser} 644 | for _, in := range input { 645 | if in != nil { 646 | bytes, err := ioutil.ReadAll(in) 647 | if err != nil { 648 | return err 649 | } 650 | if string(bytes) != ioTestText { 651 | return fmt.Errorf("Expected to read %q. Read %q instead.", ioTestText, string(bytes)) 652 | } 653 | closer, ok := in.(io.Closer) 654 | if ok { 655 | err = closer.Close() 656 | if err != nil { 657 | return err 658 | } 659 | } 660 | } 661 | } 662 | output := []io.Writer{r.Writer, r.WriteCloser} 663 | for _, out := range output { 664 | if out != nil { 665 | _, err := io.WriteString(out, ioTestText) 666 | if err != nil { 667 | return err 668 | } 669 | closer, ok := out.(io.Closer) 670 | if ok { 671 | err = closer.Close() 672 | if err != nil { 673 | return err 674 | } 675 | } 676 | } 677 | } 678 | return nil 679 | } 680 | 681 | type ioFieldTest struct { 682 | Args []string 683 | Valid bool 684 | Field string 685 | InFiles []string 686 | OutFiles []string 687 | SkipReason string 688 | } 689 | 690 | var ioFieldTests = []ioFieldTest{ 691 | // No-op 692 | {Args: []string{}, Valid: true, InFiles: []string{}, OutFiles: []string{}}, 693 | 694 | // io.Reader 695 | {Args: []string{"--reader", "-"}, Valid: true, Field: "Reader", InFiles: []string{"stdin"}, OutFiles: []string{}}, 696 | {Args: []string{"--reader", "infile"}, Valid: true, Field: "Reader", InFiles: []string{"infile"}, OutFiles: []string{}}, 697 | {Args: []string{"--reader", "bogus/infile"}, Valid: false}, 698 | {Args: []string{"--reader", ""}, Valid: false}, 699 | {Args: []string{"--reader", "infile1", "--reader", "intput2"}, Valid: false}, 700 | {Args: []string{"--reader", "-", "--reader", "intput"}, Valid: false}, 701 | {Args: []string{"--reader", "infile", "--reader", "-"}, Valid: false}, 702 | {Args: []string{"--reader"}, Valid: false}, 703 | 704 | // io.ReadCloser 705 | {Args: []string{"--readcloser", "-"}, Valid: true, Field: "ReadCloser", InFiles: []string{"stdin"}, OutFiles: []string{}}, 706 | {Args: []string{"--readcloser", "infile"}, Valid: true, Field: "ReadCloser", InFiles: []string{"infile"}, OutFiles: []string{}}, 707 | {Args: []string{"--readcloser", "bogus/infile"}, Valid: false}, 708 | {Args: []string{"--readcloser", ""}, Valid: false}, 709 | {Args: []string{"--readcloser", "infile1", "--readcloser", "intput2"}, Valid: false}, 710 | {Args: []string{"--readcloser", "-", "--readcloser", "intput"}, Valid: false}, 711 | {Args: []string{"--readcloser", "infile", "--readcloser", "-"}, Valid: false}, 712 | {Args: []string{"--readcloser"}, Valid: false}, 713 | 714 | // io.Writer 715 | {Args: []string{"--writer", "-"}, Valid: true, Field: "Writer", InFiles: []string{}, OutFiles: []string{"stdout"}}, 716 | {Args: []string{"--writer", "outfile"}, Valid: true, Field: "Writer", InFiles: []string{}, OutFiles: []string{"outfile"}}, 717 | {Args: []string{"--writer", ""}, Valid: false}, 718 | {Args: []string{"--writer", "bogus/outfile"}, Valid: false}, 719 | {Args: []string{"--writer", "outfile1", "--writer", "outfile2"}, Valid: false}, 720 | {Args: []string{"--writer", "-", "--writer", "outfile"}, Valid: false}, 721 | {Args: []string{"--writer", "outfile", "--writer", "-"}, Valid: false}, 722 | {Args: []string{"--writer"}, Valid: false}, 723 | 724 | // io.WriteCloser 725 | {Args: []string{"--writecloser", "-"}, Valid: true, Field: "WriteCloser", InFiles: []string{}, OutFiles: []string{"stdout"}}, 726 | {Args: []string{"--writecloser", "outfile"}, Valid: true, Field: "WriteCloser", InFiles: []string{}, OutFiles: []string{"outfile"}}, 727 | {Args: []string{"--writecloser", ""}, Valid: false}, 728 | {Args: []string{"--writecloser", "bogus/outfile"}, Valid: false}, 729 | {Args: []string{"--writecloser", "outfile1", "--writecloser", "outfile2"}, Valid: false}, 730 | {Args: []string{"--writecloser", "-", "--writecloser", "outfile"}, Valid: false}, 731 | {Args: []string{"--writecloser", "outfile", "--writecloser", "-"}, Valid: false}, 732 | {Args: []string{"--writecloser"}, Valid: false}, 733 | } 734 | 735 | func TestIOFields(t *testing.T) { 736 | for _, test := range ioFieldTests { 737 | spec := &ioFieldSpec{} 738 | runIOFieldTest(t, spec, test) 739 | } 740 | } 741 | 742 | func runIOFieldTest(t *testing.T, spec *ioFieldSpec, test ioFieldTest) { 743 | if test.SkipReason != "" { 744 | t.Logf("Test skipped. Args: %q, Field: %s, Reason: %s", test.Args, test.Field, test.SkipReason) 745 | return 746 | } 747 | 748 | realin, realout := os.Stdin, os.Stdout 749 | defer restoreStdinStdout(realin, realout) 750 | 751 | realdir, err := os.Getwd() 752 | if err != nil { 753 | t.Errorf("Failed to get working dir. Args: %q, Field: %s, Error: %s", test.Args, test.Field, err) 754 | return 755 | } 756 | defer restoreWorkingDir(realdir) 757 | 758 | err = setupIOFieldTest(test) 759 | if err != nil { 760 | t.Errorf("Failed to setup test. Args: %q, Field: %s, Error: %s", test.Args, test.Field, err) 761 | return 762 | } 763 | 764 | cmd := New("test", spec) 765 | _, _, err = cmd.Decode(test.Args) 766 | if !test.Valid { 767 | if err == nil { 768 | t.Errorf("Expected error but none received. Args: %q", test.Args) 769 | } 770 | return 771 | } 772 | if err != nil { 773 | t.Errorf("Received unexpected decode error. Args: %q, Field: %s, Error: %s", test.Args, test.Field, err) 774 | return 775 | } 776 | 777 | err = validateIOFieldTest(spec, test) 778 | if err != nil { 779 | t.Errorf("Validation failed during IO field test. Args: %q, Field: %s, Error: %s", test.Args, test.Field, err) 780 | return 781 | } 782 | } 783 | 784 | func restoreStdinStdout(stdin *os.File, stdout *os.File) { 785 | os.Stdin = stdin 786 | os.Stdout = stdout 787 | } 788 | 789 | func restoreWorkingDir(dir string) { 790 | err := os.Chdir(dir) 791 | if err != nil { 792 | panic(fmt.Sprintf("Failed to restore working dir. Dir: %q, Error: %s", dir, err)) 793 | } 794 | } 795 | 796 | func setupIOFieldTest(test ioFieldTest) error { 797 | tmpdir, err := ioutil.TempDir("", "writ-iofieldtest") 798 | if err != nil { 799 | return err 800 | } 801 | err = os.Chdir(tmpdir) 802 | if err != nil { 803 | return err 804 | } 805 | 806 | for _, name := range test.InFiles { 807 | f, err := os.Create(name) 808 | if err != nil { 809 | return err 810 | } 811 | _, err = io.WriteString(f, ioTestText) 812 | if err != nil { 813 | return err 814 | } 815 | err = f.Close() 816 | if err != nil { 817 | return err 818 | } 819 | if name == "stdin" { 820 | in, err := os.Open(name) 821 | if err != nil { 822 | return err 823 | } 824 | os.Stdin = in 825 | } 826 | } 827 | for _, name := range test.OutFiles { 828 | if name == "stdout" { 829 | out, err := os.Create(name) 830 | if err != nil { 831 | return err 832 | } 833 | os.Stdout = out 834 | } 835 | } 836 | return nil 837 | } 838 | 839 | func validateIOFieldTest(spec *ioFieldSpec, test ioFieldTest) error { 840 | err := spec.PerformIO() 841 | if err != nil { 842 | return err 843 | } 844 | for _, name := range test.OutFiles { 845 | in, err := os.Open(name) 846 | if err != nil { 847 | return err 848 | } 849 | bytes, err := ioutil.ReadAll(in) 850 | if err != nil { 851 | return err 852 | } 853 | if string(bytes) != ioTestText { 854 | return fmt.Errorf("Expected to read %q. Read %q instead.", ioTestText, string(bytes)) 855 | } 856 | err = in.Close() 857 | if err != nil { 858 | return err 859 | } 860 | } 861 | return nil 862 | } 863 | 864 | /* 865 | * Test custom flag and option decoders 866 | */ 867 | 868 | type customTestFlag struct { 869 | val *bool 870 | } 871 | 872 | func (d customTestFlag) Decode(arg string) error { 873 | *d.val = true 874 | return nil 875 | } 876 | 877 | type customTestFlagPtr struct { 878 | val bool 879 | } 880 | 881 | func (d *customTestFlagPtr) Decode(arg string) error { 882 | d.val = true 883 | return nil 884 | } 885 | 886 | type customTestOption struct { 887 | val *string 888 | } 889 | 890 | func (d customTestOption) Decode(arg string) error { 891 | if strings.HasPrefix(arg, "foo") { 892 | *d.val = arg 893 | return nil 894 | } 895 | return fmt.Errorf("customTestOption values must begin with foo") 896 | } 897 | 898 | type customTestOptionPtr struct { 899 | val string 900 | } 901 | 902 | func (d *customTestOptionPtr) Decode(arg string) error { 903 | if strings.HasPrefix(arg, "foo") { 904 | d.val = arg 905 | return nil 906 | } 907 | return fmt.Errorf("customTestOptionPtr values must begin with foo") 908 | } 909 | 910 | type customDecoderFieldSpec struct { 911 | CustomFlag customTestFlag `flag:"flag" description:"a custom flag field"` 912 | CustomFlagPtr customTestFlagPtr `flag:"flagptr" description:"a custom flag field with pointer receiver"` 913 | CustomOption customTestOption `option:"opt" description:"a custom option field"` 914 | CustomOptionPtr customTestOptionPtr `option:"optptr" description:"a custom option field with pointer receiver"` 915 | } 916 | 917 | var trueval = true 918 | var foobarval = "foobar" 919 | 920 | var customDecoderFieldTests = []fieldTest{ 921 | // Custom flag 922 | {Args: []string{"--flag"}, Valid: true, Field: "CustomFlag", Value: customTestFlag{val: &trueval}}, 923 | {Args: []string{"--flag", "--flag"}, Valid: false}, // Plural must be set explicitly 924 | 925 | // Custom flag with pointer receiver 926 | {Args: []string{"--flagptr"}, Valid: true, Field: "CustomFlagPtr", Value: customTestFlagPtr{val: true}}, 927 | {Args: []string{"--flagptr", "--flagptr"}, Valid: false}, // Plural must be set explicitly 928 | 929 | // Custom option 930 | {Args: []string{"--opt", "foobar"}, Valid: true, Field: "CustomOption", Value: customTestOption{val: &foobarval}}, 931 | {Args: []string{"--opt=foobar"}, Valid: true, Field: "CustomOption", Value: customTestOption{val: &foobarval}}, 932 | {Args: []string{"-opt=puppies"}, Valid: false}, 933 | {Args: []string{"-opt", "puppies"}, Valid: false}, 934 | {Args: []string{"--opt"}, Valid: false}, 935 | {Args: []string{"--opt", "foobar", "-opt", "foobar"}, Valid: false}, // Plural must be set explicitly 936 | 937 | // Custom option with pointer receiver 938 | {Args: []string{"--optptr", "foobar"}, Valid: true, Field: "CustomOptionPtr", Value: customTestOptionPtr{val: "foobar"}}, 939 | {Args: []string{"--optptr=foobar"}, Valid: true, Field: "CustomOptionPtr", Value: customTestOptionPtr{val: "foobar"}}, 940 | {Args: []string{"-optptr=puppies"}, Valid: false}, 941 | {Args: []string{"-optptr", "puppies"}, Valid: false}, 942 | {Args: []string{"--optptr"}, Valid: false}, 943 | {Args: []string{"--optptr", "foobar", "-optptr", "foobar"}, Valid: false}, // Plural must be set explicitly 944 | } 945 | 946 | func TestCustomDecoderFields(t *testing.T) { 947 | for _, test := range customDecoderFieldTests { 948 | var flagval bool 949 | var optval string 950 | spec := &customDecoderFieldSpec{ 951 | CustomFlag: customTestFlag{&flagval}, 952 | CustomOption: customTestOption{&optval}, 953 | } 954 | runFieldTest(t, spec, test) 955 | } 956 | } 957 | 958 | /* 959 | * Test basic field types 960 | */ 961 | 962 | type basicFieldSpec struct { 963 | Int int `option:"int" description:"An int option" placeholder:"INT"` 964 | Int8 int8 `option:"int8" description:"An int8 option" placeholder:"INT8"` 965 | Int16 int16 `option:"int16" description:"An int16 option" placeholder:"INT16"` 966 | Int32 int32 `option:"int32" description:"An int32 option" placeholder:"INT32"` 967 | Int64 int64 `option:"int64" description:"An int64 option" placeholder:"INT64"` 968 | Uint uint `option:"uint" description:"A uint option" placeholder:"UINT"` 969 | Uint8 uint8 `option:"uint8" description:"A uint8 option" placeholder:"UINT8"` 970 | Uint16 uint16 `option:"uint16" description:"A uint16 option" placeholder:"UINT16"` 971 | Uint32 uint32 `option:"uint32" description:"A uint32 option" placeholder:"UINT32"` 972 | Uint64 uint64 `option:"uint64" description:"A uint64 option" placeholder:"UINT64"` 973 | Float32 float32 `option:"float32" description:"A float32 option" placeholder:"FLOAT32"` 974 | Float64 float64 `option:"float64" description:"A float64 option" placeholder:"FLOAT64"` 975 | String string `option:"string" description:"A string option" placeholder:"STRING"` 976 | } 977 | 978 | var basicFieldTests = []fieldTest{ 979 | // String 980 | {Args: []string{"--string", "1"}, Valid: true, Field: "String", Value: "1"}, 981 | {Args: []string{"--string", "-1"}, Valid: true, Field: "String", Value: "-1"}, 982 | {Args: []string{"--string", "+1"}, Valid: true, Field: "String", Value: "+1"}, 983 | {Args: []string{"--string", "1.0"}, Valid: true, Field: "String", Value: "1.0"}, 984 | {Args: []string{"--string", "0x01"}, Valid: true, Field: "String", Value: "0x01"}, 985 | {Args: []string{"--string", "-"}, Valid: true, Field: "String", Value: "-"}, 986 | {Args: []string{"--string", "-a"}, Valid: true, Field: "String", Value: "-a"}, 987 | {Args: []string{"--string", "--"}, Valid: true, Field: "String", Value: "--"}, 988 | {Args: []string{"--string", "--a"}, Valid: true, Field: "String", Value: "--a"}, 989 | {Args: []string{"--string", ""}, Valid: true, Field: "String", Value: ""}, 990 | {Args: []string{"--string", " "}, Valid: true, Field: "String", Value: " "}, 991 | {Args: []string{"--string", " a"}, Valid: true, Field: "String", Value: " a"}, 992 | {Args: []string{"--string", "a "}, Valid: true, Field: "String", Value: "a "}, 993 | {Args: []string{"--string", "a b "}, Valid: true, Field: "String", Value: "a b "}, 994 | {Args: []string{"--string", " a b"}, Valid: true, Field: "String", Value: " a b"}, 995 | {Args: []string{"--string", "\n"}, Valid: true, Field: "String", Value: "\n"}, 996 | {Args: []string{"--string", "\t"}, Valid: true, Field: "String", Value: "\t"}, 997 | {Args: []string{"--string", "日本"}, Valid: true, Field: "String", Value: "日本"}, 998 | {Args: []string{"--string", "-日本"}, Valid: true, Field: "String", Value: "-日本"}, 999 | {Args: []string{"--string", "--日本"}, Valid: true, Field: "String", Value: "--日本"}, 1000 | {Args: []string{"--string", " 日本"}, Valid: true, Field: "String", Value: " 日本"}, 1001 | {Args: []string{"--string", "日本 "}, Valid: true, Field: "String", Value: "日本 "}, 1002 | {Args: []string{"--string", "日 本"}, Valid: true, Field: "String", Value: "日 本"}, 1003 | {Args: []string{"--string", "A relatively long string to make sure we aren't doing any silly truncation anywhere, since that would be bad..."}, Valid: true, Field: "String", Value: "A relatively long string to make sure we aren't doing any silly truncation anywhere, since that would be bad..."}, 1004 | {Args: []string{"--string", "a", "--string", "b"}, Valid: false}, 1005 | {Args: []string{"--string"}, Valid: false}, 1006 | 1007 | // Int8 1008 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: true, Field: "Int8", Value: int8(math.MinInt8)}, 1009 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Int8", Value: int8(math.MaxInt8)}, 1010 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: false}, 1011 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: false}, 1012 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: false}, 1013 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: false}, 1014 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: false}, 1015 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: false}, 1016 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: false}, 1017 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1018 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: false}, 1019 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: false}, 1020 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: false}, 1021 | {Args: []string{"--int8", fmt.Sprintf("%d", int64(math.MaxInt64))}, Valid: false}, 1022 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: false}, 1023 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: false}, 1024 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: false}, 1025 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: false}, 1026 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: false}, 1027 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: false}, 1028 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxUint32+1))}, Valid: false}, 1029 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1030 | {Args: []string{"--int8", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1031 | {Args: []string{"--int8", "1", "--int8", "2"}, Valid: false}, 1032 | {Args: []string{"--int8", "1.0"}, Valid: false}, 1033 | {Args: []string{"--int8", ""}, Valid: false}, 1034 | {Args: []string{"--int8"}, Valid: false}, 1035 | 1036 | // Int16 1037 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: true, Field: "Int16", Value: int16(math.MinInt8)}, 1038 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: true, Field: "Int16", Value: int16(math.MinInt8 - 1)}, 1039 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Int16", Value: int16(math.MaxInt8)}, 1040 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Int16", Value: int16(math.MaxInt8 + 1)}, 1041 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: true, Field: "Int16", Value: int16(math.MinInt16)}, 1042 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: true, Field: "Int16", Value: int16(math.MaxInt16)}, 1043 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Int16", Value: int16(math.MaxUint8)}, 1044 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: true, Field: "Int16", Value: int16(math.MaxUint8 + 1)}, 1045 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: false}, 1046 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: false}, 1047 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: false}, 1048 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1049 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: false}, 1050 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: false}, 1051 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: false}, 1052 | {Args: []string{"--int16", fmt.Sprintf("%d", int64(math.MaxInt64))}, Valid: false}, 1053 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: false}, 1054 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: false}, 1055 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: false}, 1056 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: false}, 1057 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxUint32+1))}, Valid: false}, 1058 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1059 | {Args: []string{"--int16", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1060 | {Args: []string{"--int16", "1", "--int16", "2"}, Valid: false}, 1061 | {Args: []string{"--int16", "1.0"}, Valid: false}, 1062 | {Args: []string{"--int16", ""}, Valid: false}, 1063 | {Args: []string{"--int16"}, Valid: false}, 1064 | 1065 | // Int32 1066 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: true, Field: "Int32", Value: int32(math.MinInt8)}, 1067 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: true, Field: "Int32", Value: int32(math.MinInt8 - 1)}, 1068 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Int32", Value: int32(math.MaxInt8)}, 1069 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Int32", Value: int32(math.MaxInt8 + 1)}, 1070 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: true, Field: "Int32", Value: int32(math.MinInt16)}, 1071 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: true, Field: "Int32", Value: int32(math.MinInt16 - 1)}, 1072 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: true, Field: "Int32", Value: int32(math.MaxInt16)}, 1073 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: true, Field: "Int32", Value: int32(math.MaxInt16 + 1)}, 1074 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: true, Field: "Int32", Value: int32(math.MinInt32)}, 1075 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: true, Field: "Int32", Value: int32(math.MaxInt32)}, 1076 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Int32", Value: int32(math.MaxUint8)}, 1077 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: true, Field: "Int32", Value: int32(math.MaxUint8 + 1)}, 1078 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: true, Field: "Int32", Value: int32(math.MaxUint16)}, 1079 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: true, Field: "Int32", Value: int32(math.MaxUint16 + 1)}, 1080 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1081 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: false}, 1082 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: false}, 1083 | {Args: []string{"--int32", fmt.Sprintf("%d", int64(math.MaxInt64))}, Valid: false}, 1084 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: false}, 1085 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: false}, 1086 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxUint32+1))}, Valid: false}, 1087 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1088 | {Args: []string{"--int32", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1089 | {Args: []string{"--int32", "1", "--int32", "2"}, Valid: false}, 1090 | {Args: []string{"--int32", "1.0"}, Valid: false}, 1091 | {Args: []string{"--int32", ""}, Valid: false}, 1092 | {Args: []string{"--int32"}, Valid: false}, 1093 | 1094 | // Int64 1095 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: true, Field: "Int64", Value: int64(math.MinInt8)}, 1096 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: true, Field: "Int64", Value: int64(math.MinInt8 - 1)}, 1097 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Int64", Value: int64(math.MaxInt8)}, 1098 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Int64", Value: int64(math.MaxInt8 + 1)}, 1099 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: true, Field: "Int64", Value: int64(math.MinInt16)}, 1100 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: true, Field: "Int64", Value: int64(math.MinInt16 - 1)}, 1101 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: true, Field: "Int64", Value: int64(math.MaxInt16)}, 1102 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: true, Field: "Int64", Value: int64(math.MaxInt16 + 1)}, 1103 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: true, Field: "Int64", Value: int64(math.MinInt32)}, 1104 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: true, Field: "Int64", Value: int64(math.MinInt32 - 1)}, 1105 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: true, Field: "Int64", Value: int64(math.MaxInt32)}, 1106 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: true, Field: "Int64", Value: int64(math.MaxInt32 + 1)}, 1107 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: true, Field: "Int64", Value: int64(math.MinInt64)}, 1108 | {Args: []string{"--int64", fmt.Sprintf("%d", int64(math.MaxInt64))}, Valid: true, Field: "Int64", Value: int64(math.MaxInt64)}, 1109 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Int64", Value: int64(math.MaxUint8)}, 1110 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: true, Field: "Int64", Value: int64(math.MaxUint8 + 1)}, 1111 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: true, Field: "Int64", Value: int64(math.MaxUint16)}, 1112 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: true, Field: "Int64", Value: int64(math.MaxUint16 + 1)}, 1113 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: true, Field: "Int64", Value: int64(math.MaxUint32)}, 1114 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxUint32+1))}, Valid: true, Field: "Int64", Value: int64(math.MaxUint32 + 1)}, 1115 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: false}, 1116 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1117 | {Args: []string{"--int64", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1118 | {Args: []string{"--int64", "1", "--int64", "2"}, Valid: false}, 1119 | {Args: []string{"--int64", "1.0"}, Valid: false}, 1120 | {Args: []string{"--int64", ""}, Valid: false}, 1121 | {Args: []string{"--int64"}, Valid: false}, 1122 | 1123 | // Int 1124 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: true, Field: "Int", Value: int(math.MinInt8)}, 1125 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: true, Field: "Int", Value: int(math.MinInt8 - 1)}, 1126 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Int", Value: int(math.MaxInt8)}, 1127 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Int", Value: int(math.MaxInt8 + 1)}, 1128 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: true, Field: "Int", Value: int(math.MinInt16)}, 1129 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: true, Field: "Int", Value: int(math.MinInt16 - 1)}, 1130 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: true, Field: "Int", Value: int(math.MaxInt16)}, 1131 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: true, Field: "Int", Value: int(math.MaxInt16 + 1)}, 1132 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: true, Field: "Int", Value: int(math.MinInt32)}, 1133 | {Args: []string{"--int", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: true, Field: "Int", Value: int(math.MaxInt32)}, 1134 | {Args: []string{"--int", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Int", Value: int(math.MaxUint8)}, 1135 | {Args: []string{"--int", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: true, Field: "Int", Value: int(math.MaxUint8 + 1)}, 1136 | {Args: []string{"--int", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: true, Field: "Int", Value: int(math.MaxUint16)}, 1137 | {Args: []string{"--int", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: true, Field: "Int", Value: int(math.MaxUint16 + 1)}, 1138 | {Args: []string{"--int", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: false}, 1139 | {Args: []string{"--int", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1140 | {Args: []string{"--int", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1141 | {Args: []string{"--int", "1", "--int", "2"}, Valid: false}, 1142 | {Args: []string{"--int", "1.0"}, Valid: false}, 1143 | {Args: []string{"--int", ""}, Valid: false}, 1144 | {Args: []string{"--int"}, Valid: false}, 1145 | 1146 | // Uint8 1147 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Uint8", Value: uint8(math.MaxInt8)}, 1148 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Uint8", Value: uint8(math.MaxInt8 + 1)}, 1149 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Uint8", Value: uint8(math.MaxUint8)}, 1150 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: false}, 1151 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: false}, 1152 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: false}, 1153 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: false}, 1154 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: false}, 1155 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: false}, 1156 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: false}, 1157 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1158 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: false}, 1159 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: false}, 1160 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: false}, 1161 | {Args: []string{"--uint8", fmt.Sprintf("%d", int64(math.MaxInt64))}, Valid: false}, 1162 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: false}, 1163 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: false}, 1164 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: false}, 1165 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: false}, 1166 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: false}, 1167 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxUint32+1))}, Valid: false}, 1168 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1169 | {Args: []string{"--uint8", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1170 | {Args: []string{"--uint8", "1", "--uint8", "2"}, Valid: false}, 1171 | {Args: []string{"--uint8", "1.0"}, Valid: false}, 1172 | {Args: []string{"--uint8", ""}, Valid: false}, 1173 | {Args: []string{"--uint8"}, Valid: false}, 1174 | 1175 | // Uint16 1176 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Uint16", Value: uint16(math.MaxInt8)}, 1177 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Uint16", Value: uint16(math.MaxInt8 + 1)}, 1178 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: true, Field: "Uint16", Value: uint16(math.MaxInt16)}, 1179 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: true, Field: "Uint16", Value: uint16(math.MaxInt16 + 1)}, 1180 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Uint16", Value: uint16(math.MaxUint8)}, 1181 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: true, Field: "Uint16", Value: uint16(math.MaxUint8 + 1)}, 1182 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: true, Field: "Uint16", Value: uint16(math.MaxUint16)}, 1183 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: false}, 1184 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: false}, 1185 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: false}, 1186 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: false}, 1187 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: false}, 1188 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1189 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: false}, 1190 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: false}, 1191 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: false}, 1192 | {Args: []string{"--uint16", fmt.Sprintf("%d", int64(math.MaxInt64))}, Valid: false}, 1193 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: false}, 1194 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: false}, 1195 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: false}, 1196 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxUint32+1))}, Valid: false}, 1197 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1198 | {Args: []string{"--uint16", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1199 | {Args: []string{"--uint16", "1", "--uint16", "2"}, Valid: false}, 1200 | {Args: []string{"--uint16", "1.0"}, Valid: false}, 1201 | {Args: []string{"--uint16", ""}, Valid: false}, 1202 | {Args: []string{"--uint16"}, Valid: false}, 1203 | 1204 | // Uint32 1205 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxInt8)}, 1206 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxInt8 + 1)}, 1207 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxInt16)}, 1208 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxInt16 + 1)}, 1209 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxInt32)}, 1210 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxInt32 + 1)}, 1211 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxUint8)}, 1212 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxUint8 + 1)}, 1213 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxUint16)}, 1214 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxUint16 + 1)}, 1215 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: true, Field: "Uint32", Value: uint32(math.MaxUint32)}, 1216 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: false}, 1217 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: false}, 1218 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: false}, 1219 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: false}, 1220 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: false}, 1221 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1222 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: false}, 1223 | {Args: []string{"--uint32", fmt.Sprintf("%d", int64(math.MaxInt64))}, Valid: false}, 1224 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: false}, 1225 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxUint32+1))}, Valid: false}, 1226 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1227 | {Args: []string{"--uint32", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: false}, 1228 | {Args: []string{"--uint32", "1", "--uint32", "1"}, Valid: false}, 1229 | {Args: []string{"--uint32", "1.0"}, Valid: false}, 1230 | {Args: []string{"--uint32", ""}, Valid: false}, 1231 | {Args: []string{"--uint32"}, Valid: false}, 1232 | 1233 | // Uint64 1234 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxInt8)}, 1235 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxInt8 + 1)}, 1236 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxInt16)}, 1237 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxInt16 + 1)}, 1238 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxInt32)}, 1239 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxInt32 + 1)}, 1240 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MaxInt64))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxInt64)}, 1241 | {Args: []string{"--uint64", fmt.Sprintf("%d", uint64(math.MaxInt64+1))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxInt64 + 1)}, 1242 | {Args: []string{"--uint64", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxUint8)}, 1243 | {Args: []string{"--uint64", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxUint8 + 1)}, 1244 | {Args: []string{"--uint64", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxUint16)}, 1245 | {Args: []string{"--uint64", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxUint16 + 1)}, 1246 | {Args: []string{"--uint64", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxUint32)}, 1247 | {Args: []string{"--uint64", fmt.Sprintf("%d", uint64(math.MaxUint32+1))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxUint32 + 1)}, 1248 | {Args: []string{"--uint64", fmt.Sprintf("%d", uint64(math.MaxUint64))}, Valid: true, Field: "Uint64", Value: uint64(math.MaxUint64)}, 1249 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: false}, 1250 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: false}, 1251 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: false}, 1252 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: false}, 1253 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: false}, 1254 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1255 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: false}, 1256 | {Args: []string{"--uint64", fmt.Sprintf("%d", int64(math.MinInt64))}, Valid: false}, 1257 | {Args: []string{"--uint64", "1", "--uint64", "1"}, Valid: false}, 1258 | {Args: []string{"--uint64", "1.0"}, Valid: false}, 1259 | {Args: []string{"--uint64", ""}, Valid: false}, 1260 | {Args: []string{"--uint64"}, Valid: false}, 1261 | 1262 | // Uint 1263 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MaxInt8))}, Valid: true, Field: "Uint", Value: uint(math.MaxInt8)}, 1264 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MaxInt8+1))}, Valid: true, Field: "Uint", Value: uint(math.MaxInt8 + 1)}, 1265 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MaxInt16))}, Valid: true, Field: "Uint", Value: uint(math.MaxInt16)}, 1266 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MaxInt16+1))}, Valid: true, Field: "Uint", Value: uint(math.MaxInt16 + 1)}, 1267 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MaxInt32))}, Valid: true, Field: "Uint", Value: uint(math.MaxInt32)}, 1268 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MaxInt32+1))}, Valid: true, Field: "Uint", Value: uint(math.MaxInt32 + 1)}, 1269 | {Args: []string{"--uint", fmt.Sprintf("%d", uint64(math.MaxUint8))}, Valid: true, Field: "Uint", Value: uint(math.MaxUint8)}, 1270 | {Args: []string{"--uint", fmt.Sprintf("%d", uint64(math.MaxUint8+1))}, Valid: true, Field: "Uint", Value: uint(math.MaxUint8 + 1)}, 1271 | {Args: []string{"--uint", fmt.Sprintf("%d", uint64(math.MaxUint16))}, Valid: true, Field: "Uint", Value: uint(math.MaxUint16)}, 1272 | {Args: []string{"--uint", fmt.Sprintf("%d", uint64(math.MaxUint16+1))}, Valid: true, Field: "Uint", Value: uint(math.MaxUint16 + 1)}, 1273 | {Args: []string{"--uint", fmt.Sprintf("%d", uint64(math.MaxUint32))}, Valid: true, Field: "Uint", Value: uint(math.MaxUint32)}, 1274 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MinInt8))}, Valid: false}, 1275 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MinInt8-1))}, Valid: false}, 1276 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MinInt16))}, Valid: false}, 1277 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MinInt16-1))}, Valid: false}, 1278 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MinInt32))}, Valid: false}, 1279 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1280 | {Args: []string{"--uint", fmt.Sprintf("%d", int64(math.MinInt32-1))}, Valid: false}, 1281 | {Args: []string{"--uint", "1", "--uint", "2"}, Valid: false}, 1282 | {Args: []string{"--uint", "1.0"}, Valid: false}, 1283 | {Args: []string{"--uint", ""}, Valid: false}, 1284 | {Args: []string{"--uint"}, Valid: false}, 1285 | 1286 | // Float32 1287 | {Args: []string{"--float32", "-1.23"}, Valid: true, Field: "Float32", Value: float32(-1.23)}, 1288 | {Args: []string{"--float32", "4.56"}, Valid: true, Field: "Float32", Value: float32(4.56)}, 1289 | {Args: []string{"--float32", "-1.2e3"}, Valid: true, Field: "Float32", Value: float32(-1.2e3)}, 1290 | {Args: []string{"--float32", "4.5e6"}, Valid: true, Field: "Float32", Value: float32(4.5e6)}, 1291 | {Args: []string{"--float32", "-1.2E3"}, Valid: true, Field: "Float32", Value: float32(-1.2e3)}, 1292 | {Args: []string{"--float32", "4.5E6"}, Valid: true, Field: "Float32", Value: float32(4.5e6)}, 1293 | {Args: []string{"--float32", "-1.2e+3"}, Valid: true, Field: "Float32", Value: float32(-1.2e3)}, 1294 | {Args: []string{"--float32", "4.5e+6"}, Valid: true, Field: "Float32", Value: float32(4.5e6)}, 1295 | {Args: []string{"--float32", "-1.2E+3"}, Valid: true, Field: "Float32", Value: float32(-1.2e3)}, 1296 | {Args: []string{"--float32", "4.5E+6"}, Valid: true, Field: "Float32", Value: float32(4.5e6)}, 1297 | {Args: []string{"--float32", "-1.2e-3"}, Valid: true, Field: "Float32", Value: float32(-1.2e-3)}, 1298 | {Args: []string{"--float32", "4.5e-6"}, Valid: true, Field: "Float32", Value: float32(4.5e-6)}, 1299 | {Args: []string{"--float32", "-1.2E-3"}, Valid: true, Field: "Float32", Value: float32(-1.2e-3)}, 1300 | {Args: []string{"--float32", "4.5E-6"}, Valid: true, Field: "Float32", Value: float32(4.5e-6)}, 1301 | {Args: []string{"--float32", strconv.FormatFloat(math.SmallestNonzeroFloat32, 'f', -1, 64)}, Valid: true, Field: "Float32", Value: float32(math.SmallestNonzeroFloat32)}, 1302 | {Args: []string{"--float32", strconv.FormatFloat(math.MaxFloat32, 'f', -1, 64)}, Valid: true, Field: "Float32", Value: float32(math.MaxFloat32)}, 1303 | {Args: []string{"--float32", strconv.FormatFloat(math.MaxFloat32, 'f', -1, 64)}, Valid: true, Field: "Float32", Value: float32(math.MaxFloat32)}, 1304 | // XXX Skipped -- Not sure how to handle this!! 1305 | {Args: []string{"--float32", strconv.FormatFloat(math.SmallestNonzeroFloat64, 'f', -1, 64)}, Field: "Float32", SkipReason: "Not sure how to handle the precision on this"}, 1306 | {Args: []string{"--float32", strconv.FormatFloat(math.MaxFloat64, 'f', -1, 64)}, Valid: false}, 1307 | {Args: []string{"--float32", strconv.FormatFloat(math.MaxFloat64, 'f', -1, 64)}, Valid: false}, 1308 | {Args: []string{"--float32", "1"}, Valid: true, Field: "Float32", Value: float32(1)}, 1309 | {Args: []string{"--float32", "-1"}, Valid: true, Field: "Float32", Value: float32(-1)}, 1310 | {Args: []string{"--float32", "1.0", "--float32", "2.0"}, Valid: false}, 1311 | {Args: []string{"--float32", ""}, Valid: false}, 1312 | {Args: []string{"--float32"}, Valid: false}, 1313 | 1314 | // Float64 1315 | {Args: []string{"--float64", "-1.23"}, Valid: true, Field: "Float64", Value: float64(-1.23)}, 1316 | {Args: []string{"--float64", "4.56"}, Valid: true, Field: "Float64", Value: float64(4.56)}, 1317 | {Args: []string{"--float64", "-1.2e3"}, Valid: true, Field: "Float64", Value: float64(-1.2e3)}, 1318 | {Args: []string{"--float64", "4.5e6"}, Valid: true, Field: "Float64", Value: float64(4.5e6)}, 1319 | {Args: []string{"--float64", "-1.2E3"}, Valid: true, Field: "Float64", Value: float64(-1.2e3)}, 1320 | {Args: []string{"--float64", "4.5E6"}, Valid: true, Field: "Float64", Value: float64(4.5e6)}, 1321 | {Args: []string{"--float64", "-1.2e+3"}, Valid: true, Field: "Float64", Value: float64(-1.2e3)}, 1322 | {Args: []string{"--float64", "4.5e+6"}, Valid: true, Field: "Float64", Value: float64(4.5e6)}, 1323 | {Args: []string{"--float64", "-1.2E+3"}, Valid: true, Field: "Float64", Value: float64(-1.2e3)}, 1324 | {Args: []string{"--float64", "4.5E+6"}, Valid: true, Field: "Float64", Value: float64(4.5e6)}, 1325 | {Args: []string{"--float64", "-1.2e-3"}, Valid: true, Field: "Float64", Value: float64(-1.2e-3)}, 1326 | {Args: []string{"--float64", "4.5e-6"}, Valid: true, Field: "Float64", Value: float64(4.5e-6)}, 1327 | {Args: []string{"--float64", "-1.2E-3"}, Valid: true, Field: "Float64", Value: float64(-1.2e-3)}, 1328 | {Args: []string{"--float64", "4.5E-6"}, Valid: true, Field: "Float64", Value: float64(4.5e-6)}, 1329 | {Args: []string{"--float64", strconv.FormatFloat(math.SmallestNonzeroFloat32, 'f', -1, 64)}, Valid: true, Field: "Float64", Value: float64(math.SmallestNonzeroFloat32)}, 1330 | {Args: []string{"--float64", strconv.FormatFloat(math.MaxFloat32, 'f', -1, 64)}, Valid: true, Field: "Float64", Value: float64(math.MaxFloat32)}, 1331 | {Args: []string{"--float64", strconv.FormatFloat(math.SmallestNonzeroFloat64, 'f', -1, 64)}, Valid: true, Field: "Float64", Value: float64(math.SmallestNonzeroFloat64)}, 1332 | {Args: []string{"--float64", strconv.FormatFloat(math.MaxFloat64, 'f', -1, 64)}, Valid: true, Field: "Float64", Value: float64(math.MaxFloat64)}, 1333 | {Args: []string{"--float64", "1"}, Valid: true, Field: "Float64", Value: float64(1)}, 1334 | {Args: []string{"--float64", "-1"}, Valid: true, Field: "Float64", Value: float64(-1)}, 1335 | {Args: []string{"--float64", "1.0", "--float64", "2.0"}, Valid: false}, 1336 | {Args: []string{"--float64", ""}, Valid: false}, 1337 | {Args: []string{"--float64"}, Valid: false}, 1338 | } 1339 | 1340 | func TestBasicFields(t *testing.T) { 1341 | for _, test := range basicFieldTests { 1342 | spec := &basicFieldSpec{} 1343 | runFieldTest(t, spec, test) 1344 | } 1345 | } 1346 | 1347 | /* 1348 | * Test invalid specs 1349 | */ 1350 | 1351 | var invalidSpecTests = []struct { 1352 | Description string 1353 | Spec interface{} 1354 | }{ 1355 | // Invalid command specs 1356 | { 1357 | Description: "Commands must have a name 1", 1358 | Spec: &struct { 1359 | Command struct{} `command:","` 1360 | }{}, 1361 | }, 1362 | { 1363 | Description: "Commands must have a name 2", 1364 | Spec: &struct { 1365 | Command struct{} `command:" "` 1366 | }{}, 1367 | }, 1368 | { 1369 | Description: "Commands must have a single name", 1370 | Spec: &struct { 1371 | Command struct{} `command:"one,two"` 1372 | }{}, 1373 | }, 1374 | { 1375 | Description: "Command names cannot have a leading '-' prefix", 1376 | Spec: &struct { 1377 | Command struct{} `command:"-command"` 1378 | }{}, 1379 | }, 1380 | { 1381 | Description: "Command aliases cannot have a leading '-' prefix", 1382 | Spec: &struct { 1383 | Command struct{} `command:"command" alias:"-alias"` 1384 | }{}, 1385 | }, 1386 | { 1387 | Description: "Commands cannot have placeholders", 1388 | Spec: &struct { 1389 | Command struct{} `command:"command" placeholder:"PLACEHOLDER"` 1390 | }{}, 1391 | }, 1392 | { 1393 | Description: "Commands cannot have default values", 1394 | Spec: &struct { 1395 | Command struct{} `command:"command" default:"default"` 1396 | }{}, 1397 | }, 1398 | { 1399 | Description: "Commands cannot have env values", 1400 | Spec: &struct { 1401 | Command struct{} `command:"command" env:"ENV_VALUE"` 1402 | }{}, 1403 | }, 1404 | { 1405 | Description: "Command fields must be exported", 1406 | Spec: &struct { 1407 | command struct{} `command:"command"` 1408 | }{}, 1409 | }, 1410 | { 1411 | Description: "Command and alias names must be unique 1", 1412 | Spec: &struct { 1413 | Command struct{} `command:"foo" alias:"foo"` 1414 | }{}, 1415 | }, 1416 | { 1417 | Description: "Command and alias names must be unique 2", 1418 | Spec: &struct { 1419 | Command1 struct{} `command:"foo"` 1420 | Command2 struct{} `command:"foo"` 1421 | }{}, 1422 | }, 1423 | { 1424 | Description: "Command and alias names must be unique 3", 1425 | Spec: &struct { 1426 | Command1 struct{} `command:"foo"` 1427 | Command2 struct{} `command:"b" alias:"foo"` 1428 | }{}, 1429 | }, 1430 | { 1431 | Description: "Command and alias names must be unique 4", 1432 | Spec: &struct { 1433 | Command1 struct{} `command:"a" alias:"foo"` 1434 | Command2 struct{} `command:"b" alias:"foo"` 1435 | }{}, 1436 | }, 1437 | { 1438 | Description: "Command specs must be a pointer to struct 1", 1439 | Spec: struct{}{}, 1440 | }, 1441 | { 1442 | Description: "Command specs must be a pointer to struct 2", 1443 | Spec: 42, 1444 | }, 1445 | { 1446 | Description: "Command specs must be a pointer to struct 3", 1447 | Spec: (*int)(nil), 1448 | }, 1449 | 1450 | // Invalid option specs 1451 | { 1452 | Description: "Options cannot have aliases", 1453 | Spec: &struct { 1454 | Option int `option:"option" alias:"alias" description:"option with an alias"` 1455 | }{}, 1456 | }, 1457 | { 1458 | Description: "Options must have a name 1", 1459 | Spec: &struct { 1460 | Option int `option:"," description:"option with no name"` 1461 | }{}, 1462 | }, 1463 | { 1464 | Description: "Options must have a name 2", 1465 | Spec: &struct { 1466 | Option int `option:" " description:"option with no name"` 1467 | }{}, 1468 | }, 1469 | { 1470 | Description: "Long option names cannot have a leading '-' prefix", 1471 | Spec: &struct { 1472 | Option int `option:"-option" description:"leading dash prefix"` 1473 | }{}, 1474 | }, 1475 | { 1476 | Description: "Short option names cannot have a leading '-' prefix", 1477 | Spec: &struct { 1478 | Option int `option:"-o" description:"leading dash prefix"` 1479 | }{}, 1480 | }, 1481 | { 1482 | Description: "Option fields must be exported", 1483 | Spec: &struct { 1484 | option int `option:"option" description:"non-exported field"` 1485 | }{}, 1486 | }, 1487 | { 1488 | Description: "Bools cannot be options", 1489 | Spec: &struct { 1490 | Option bool `option:"b" description:"boolean option"` 1491 | }{}, 1492 | }, 1493 | { 1494 | Description: "Option names must be unique 1", 1495 | Spec: &struct { 1496 | Option1 int `option:"foo"` 1497 | Option2 int `option:"foo"` 1498 | }{}, 1499 | }, 1500 | { 1501 | Description: "Option names must be unique 2", 1502 | Spec: &struct { 1503 | Option1 int `option:"a, foo"` 1504 | Option2 int `option:"b, foo"` 1505 | }{}, 1506 | }, 1507 | { 1508 | Description: "Option names must be unique 3", 1509 | Spec: &struct { 1510 | Flag bool `flag:"foo"` 1511 | Option int `option:"foo"` 1512 | }{}, 1513 | }, 1514 | { 1515 | Description: "Not a supported option type", 1516 | Spec: &struct { 1517 | Option map[string]int `option:"foo"` 1518 | }{}, 1519 | }, 1520 | 1521 | // Invalid flag specs 1522 | { 1523 | Description: "Flags cannot have aliases", 1524 | Spec: &struct { 1525 | Flag bool `flag:"flag" alias:"alias" description:"flag with an alias"` 1526 | }{}, 1527 | }, 1528 | { 1529 | Description: "Flags cannot have placeholders", 1530 | Spec: &struct { 1531 | Flag bool `flag:"flag" placeholder:"PLACEHOLDER" description:"placeholder on flag"` 1532 | }{}, 1533 | }, 1534 | { 1535 | Description: "Flags cannot have default values", 1536 | Spec: &struct { 1537 | Flag bool `flag:"flag" default:"default" description:"default on flag"` 1538 | }{}, 1539 | }, 1540 | { 1541 | Description: "Flags cannot have env values", 1542 | Spec: &struct { 1543 | Flag bool `flag:"flag" env:"ENV_VALUE" description:"env on flag"` 1544 | }{}, 1545 | }, 1546 | { 1547 | Description: "Flags must have a name 1", 1548 | Spec: &struct { 1549 | Flag bool `flag:"," description:"flag with no name"` 1550 | }{}, 1551 | }, 1552 | { 1553 | Description: "Flags must have a name 2", 1554 | Spec: &struct { 1555 | Flag bool `flag:" " description:"flag with no name"` 1556 | }{}, 1557 | }, 1558 | { 1559 | Description: "Long flag names cannot have a leading '-' prefix", 1560 | Spec: &struct { 1561 | Flag bool `flag:"-flag" description:"leading dash prefix"` 1562 | }{}, 1563 | }, 1564 | { 1565 | Description: "Short flag names cannot have a leading '-' prefix", 1566 | Spec: &struct { 1567 | Flag bool `flag:"-f" description:"leading dash prefix"` 1568 | }{}, 1569 | }, 1570 | { 1571 | Description: "Flag fields must be exported", 1572 | Spec: &struct { 1573 | flag int `flag:"flag" description:"non-exported field"` 1574 | }{}, 1575 | }, 1576 | { 1577 | Description: "Flag names must be unique 1", 1578 | Spec: &struct { 1579 | Flag1 bool `flag:"foo"` 1580 | Flag2 bool `flag:"foo"` 1581 | }{}, 1582 | }, 1583 | { 1584 | Description: "Flag names must be unique 2", 1585 | Spec: &struct { 1586 | Flag1 bool `flag:"a, foo"` 1587 | Flag2 bool `flag:"b, foo"` 1588 | }{}, 1589 | }, 1590 | { 1591 | Description: "Flag names must be unique 3", 1592 | Spec: &struct { 1593 | Flag bool `flag:"foo"` 1594 | Option int `option:"foo"` 1595 | }{}, 1596 | }, 1597 | { 1598 | Description: "Flag may only be bools, ints, and OptionDecoders 1", 1599 | Spec: &struct { 1600 | Flag string `flag:"foo"` 1601 | }{}, 1602 | }, 1603 | { 1604 | Description: "Flag may only be bools, ints, and OptionDecoders 2", 1605 | Spec: &struct { 1606 | Flag int32 `flag:"foo"` 1607 | }{}, 1608 | }, 1609 | { 1610 | Description: "Flag may only be bools, ints, and OptionDecoders 3", 1611 | Spec: &struct { 1612 | Flag struct{} `flag:"foo"` 1613 | }{}, 1614 | }, 1615 | 1616 | // Invalid mixes of command, flag, and option 1617 | { 1618 | Description: "Commands cannot be options", 1619 | Spec: &struct { 1620 | Command struct{} `command:"command" option:"option" description:"command as option"` 1621 | }{}, 1622 | }, 1623 | { 1624 | Description: "Commands cannot be flags", 1625 | Spec: &struct { 1626 | Command struct{} `command:"command" flag:"flag" description:"command as flag"` 1627 | }{}, 1628 | }, 1629 | { 1630 | Description: "Options cannot be commands", 1631 | Spec: &struct { 1632 | Option int `option:"option" command:"command" description:"option as command"` 1633 | }{}, 1634 | }, 1635 | { 1636 | Description: "Options cannot be flags", 1637 | Spec: &struct { 1638 | Option int `option:"option" flag:"flag" description:"option as flag"` 1639 | }{}, 1640 | }, 1641 | { 1642 | Description: "Flags cannot be commands", 1643 | Spec: &struct { 1644 | Flag bool `flag:"flag" command:"command" description:"flag as command"` 1645 | }{}, 1646 | }, 1647 | { 1648 | Description: "Flags cannot be options", 1649 | Spec: &struct { 1650 | Flag bool `flag:"flag" option:"option" description:"flag as option"` 1651 | }{}, 1652 | }, 1653 | } 1654 | 1655 | func TestInvalidSpecs(t *testing.T) { 1656 | for _, test := range invalidSpecTests { 1657 | err := newInvalidCommand(test.Spec) 1658 | if err == nil { 1659 | t.Errorf("Expected error creating spec, but none received. Test: %s", test.Description) 1660 | continue 1661 | } 1662 | } 1663 | } 1664 | 1665 | func newInvalidCommand(spec interface{}) (err error) { 1666 | defer func() { 1667 | r := recover() 1668 | if r != nil { 1669 | switch e := r.(type) { 1670 | case commandError: 1671 | err = e 1672 | case optionError: 1673 | err = e 1674 | default: 1675 | panic(e) 1676 | } 1677 | } 1678 | }() 1679 | New("test", spec) 1680 | return nil 1681 | } 1682 | 1683 | var invalidCommandTests = []struct { 1684 | Description string 1685 | Command *Command 1686 | }{ 1687 | { 1688 | Description: "Command name cannot be empty", 1689 | Command: &Command{Name: ""}, 1690 | }, 1691 | { 1692 | Description: "Command names cannot begin with -", 1693 | Command: &Command{Name: "-command"}, 1694 | }, 1695 | { 1696 | Description: "Command aliases cannot begin with -", 1697 | Command: &Command{Name: "command", Aliases: []string{"-alias"}}, 1698 | }, 1699 | { 1700 | Description: "Command names cannot have spaces 1", 1701 | Command: &Command{Name: " command"}, 1702 | }, 1703 | { 1704 | Description: "Command names cannot have spaces 2", 1705 | Command: &Command{Name: "command "}, 1706 | }, 1707 | { 1708 | Description: "Command names cannot have spaces 3", 1709 | Command: &Command{Name: "command spaces"}, 1710 | }, 1711 | { 1712 | Description: "Command aliases cannot begin with -", 1713 | Command: &Command{Name: "command", Aliases: []string{"-alias"}}, 1714 | }, 1715 | { 1716 | Description: "Command aliases cannot have spaces 1", 1717 | Command: &Command{Name: "command", Aliases: []string{" alias"}}, 1718 | }, 1719 | { 1720 | Description: "Command aliases cannot have spaces 2", 1721 | Command: &Command{Name: "command", Aliases: []string{"alias "}}, 1722 | }, 1723 | { 1724 | Description: "Command aliases cannot have spaces 3", 1725 | Command: &Command{Name: "command", Aliases: []string{"alias spaces"}}, 1726 | }, 1727 | } 1728 | 1729 | func TestDirectCommandValidation(t *testing.T) { 1730 | for _, test := range invalidCommandTests { 1731 | err := checkInvalidCommand(test.Command) 1732 | if err == nil { 1733 | t.Errorf("Expected error validating command, but none received. Test: %s", test.Description) 1734 | continue 1735 | } 1736 | } 1737 | } 1738 | 1739 | func checkInvalidCommand(cmd *Command) (err error) { 1740 | defer func() { 1741 | r := recover() 1742 | if r != nil { 1743 | switch e := r.(type) { 1744 | case commandError: 1745 | err = e 1746 | case optionError: 1747 | err = e 1748 | default: 1749 | panic(e) 1750 | } 1751 | } 1752 | }() 1753 | cmd.validate() 1754 | return nil 1755 | } 1756 | 1757 | func TestGroupCommands(t *testing.T) { 1758 | spec := &struct { 1759 | Command1 struct{} `command:"command1"` 1760 | Command2 struct{} `command:"command2"` 1761 | }{} 1762 | cmd := New("test", spec) 1763 | 1764 | group := cmd.GroupCommands("command1") 1765 | if len(group.Commands) != 1 || group.Commands[0].Name != "command1" { 1766 | t.Errorf("Expected a single command group with command %q", "command1") 1767 | } 1768 | group = cmd.GroupCommands("command2") 1769 | if len(group.Commands) != 1 || group.Commands[0].Name != "command2" { 1770 | t.Errorf("Expected a single command group with command %q", "command2") 1771 | } 1772 | group = cmd.GroupCommands("command1", "command2") 1773 | if len(group.Commands) != 2 || group.Commands[0].Name != "command1" || group.Commands[1].Name != "command2" { 1774 | t.Errorf("Expected a single command group with commands %q and %q", "command1", "command2") 1775 | } 1776 | group = cmd.GroupCommands("command2", "command1") 1777 | if len(group.Commands) != 2 || group.Commands[0].Name != "command2" || group.Commands[1].Name != "command1" { 1778 | t.Errorf("Expected a single command group with commands %q and %q", "command2", "command1") 1779 | } 1780 | err := checkInvalidCommandGroup(cmd, "command3") 1781 | if err == nil { 1782 | t.Errorf("Expected an error to occur grouping an unknown command, but none encountered.") 1783 | } 1784 | err = checkInvalidCommandGroup(cmd, "command1", "command3") 1785 | if err == nil { 1786 | t.Errorf("Expected an error to occur grouping an unknown command, but none encountered.") 1787 | } 1788 | } 1789 | 1790 | func checkInvalidCommandGroup(cmd *Command, name ...string) (err error) { 1791 | defer func() { 1792 | r := recover() 1793 | if r != nil { 1794 | switch e := r.(type) { 1795 | case commandError: 1796 | err = e 1797 | case optionError: 1798 | err = e 1799 | default: 1800 | panic(e) 1801 | } 1802 | } 1803 | }() 1804 | cmd.GroupCommands(name...) 1805 | return nil 1806 | } 1807 | 1808 | func TestGroupOptions(t *testing.T) { 1809 | spec := &struct { 1810 | Option1 int `option:"option1"` 1811 | Option2 int `option:"option2"` 1812 | }{} 1813 | cmd := New("test", spec) 1814 | 1815 | group := cmd.GroupOptions("option1") 1816 | if len(group.Options) != 1 || group.Options[0].Names[0] != "option1" { 1817 | t.Errorf("Expected a single option group with option %q", "option1") 1818 | } 1819 | group = cmd.GroupOptions("option2") 1820 | if len(group.Options) != 1 || group.Options[0].Names[0] != "option2" { 1821 | t.Errorf("Expected a single option group with option %q", "option2") 1822 | } 1823 | group = cmd.GroupOptions("option1", "option2") 1824 | if len(group.Options) != 2 || group.Options[0].Names[0] != "option1" || group.Options[1].Names[0] != "option2" { 1825 | t.Errorf("Expected a single option group with options %q and %q", "option1", "option2") 1826 | } 1827 | group = cmd.GroupOptions("option2", "option1") 1828 | if len(group.Options) != 2 || group.Options[0].Names[0] != "option2" || group.Options[1].Names[0] != "option1" { 1829 | t.Errorf("Expected a single option group with options %q and %q", "option2", "option1") 1830 | } 1831 | err := checkInvalidOptionGroup(cmd, "option3") 1832 | if err == nil { 1833 | t.Errorf("Expected an error to occur grouping an unknown option, but none encountered.") 1834 | } 1835 | err = checkInvalidOptionGroup(cmd, "option1", "option3") 1836 | if err == nil { 1837 | t.Errorf("Expected an error to occur grouping an unknown option, but none encountered.") 1838 | } 1839 | } 1840 | 1841 | func checkInvalidOptionGroup(cmd *Command, name ...string) (err error) { 1842 | defer func() { 1843 | r := recover() 1844 | if r != nil { 1845 | switch e := r.(type) { 1846 | case commandError: 1847 | err = e 1848 | case optionError: 1849 | err = e 1850 | default: 1851 | panic(e) 1852 | } 1853 | } 1854 | }() 1855 | cmd.GroupOptions(name...) 1856 | return nil 1857 | } 1858 | 1859 | func TestCheckUnknownTagType(t *testing.T) { 1860 | defer func() { 1861 | spec := struct { 1862 | Bogus int `bogus:"bogus"` 1863 | }{} 1864 | rval := reflect.ValueOf(spec) 1865 | field, present := rval.Type().FieldByName("Bogus") 1866 | if !present { 1867 | t.Errorf("Expected Bogus field to be present") 1868 | return 1869 | } 1870 | 1871 | defer func() { recover() }() 1872 | checkTags(field, "bogus") 1873 | t.Errorf("Expected checkFields() to panic on unknown tag %q, but it didn't happen", "bogus") 1874 | }() 1875 | } 1876 | 1877 | /* 1878 | * Misc coverage tests to ensure code doesn't panic/blow-up 1879 | */ 1880 | 1881 | func TestCommandError(t *testing.T) { 1882 | err := commandError{fmt.Errorf("test")} 1883 | if err.Error() != "test" { 1884 | t.Errorf("Expected commandError to return underlying error string. Expected: %q, Received: %q", "test", err.Error()) 1885 | } 1886 | } 1887 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | /* 22 | Package writ implements a flexible option parser with thorough test coverage. 23 | It's meant to be simple and "just work". Applications using writ look and 24 | behave similar to common GNU command-line applications, making them comfortable 25 | for end-users. 26 | 27 | Writ implements option decoding with GNU getopt_long conventions. All long and 28 | short-form option variations are supported: --with-x, --name Sam, --day=Friday, 29 | -i FILE, -vvv, etc. 30 | 31 | Help output generation is supported using text/template. The default template 32 | can be overriden with a custom template. 33 | 34 | Basics 35 | 36 | Writ uses the Command and Option types to represent available options and 37 | subcommands. Input arguments are decoded with Command.Decode(). 38 | 39 | For convenience, the New() function can parse an input struct into a 40 | Command with Options that represent the input struct's fields. It uses 41 | struct field tags to control the behavior. The resulting Command's Decode() 42 | method updates the struct's fields in-place when option arguments are decoded. 43 | 44 | Alternatively, Commands and Options may be created directly. All fields on 45 | these types are exported. 46 | 47 | Options 48 | 49 | Options are specified via the "option" and "flag" struct tags. Both represent 50 | options, but fields marked "option" take arguments, whereas fields marked 51 | "flag" do not. 52 | 53 | Every Option must have an OptionDecoder. Writ provides decoders for most 54 | basic types, as well as some convenience types. See the NewOptionDecoder() 55 | function docs for details. 56 | 57 | Commands 58 | 59 | New() parses an input struct to build a top-level Command. Subcommands are 60 | supported by using the "command" field tag. Fields marked with "command" must 61 | be of struct type, and are parsed the same way as top-level commands. 62 | 63 | Help Output 64 | 65 | Writ provides methods for generating help output. Command.WriteHelp() 66 | generates help content and writes to a given io.Writer. Command.ExitHelp() 67 | writes help content to stdout or stderr and terminates the program. 68 | 69 | Writ uses a template to generate the help content. The default template 70 | mimics --help output for common GNU programs. See the documentation of the 71 | Help type for more details. 72 | 73 | Field Tag Reference 74 | 75 | The New() function recognizes the following combinations of field tags: 76 | 77 | Option Fields: 78 | - option (required): a comma-separated list of names for the option 79 | - description: the description to display for help output 80 | - placeholder: the placeholder value to use next to the option names (e.g. FILE) 81 | - default: the default value for the field 82 | - env: the name of an environment variable, the value of which is used as a default for the field 83 | 84 | Flag fields: 85 | - flag (required): a comma-separated list of names for the flag 86 | - description: the description to display for help output 87 | 88 | Command fields: 89 | - name (required): a name for the command 90 | - aliases: a comma-separated list of alias names for the command 91 | - description: the description to display for help output 92 | 93 | If both "default" and "env" are specified for an option field, the environment 94 | variable is consulted first. If the environment variable is present and 95 | decodes without error, that value is used. Otherwise, the value for the 96 | "default" tag is used. Values specified via parsed arguments take precedence 97 | over both types of defaults. 98 | */ 99 | package writ 100 | -------------------------------------------------------------------------------- /example_basic_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Bob Ziuchkovski. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package writ_test 6 | 7 | import ( 8 | "fmt" 9 | "github.com/bobziuchkovski/writ" 10 | "strings" 11 | ) 12 | 13 | type Greeter struct { 14 | HelpFlag bool `flag:"help" description:"Display this help message and exit"` 15 | Verbosity int `flag:"v, verbose" description:"Display verbose output"` 16 | Name string `option:"n, name" default:"Everyone" description:"The person or people to greet"` 17 | } 18 | 19 | // This example uses writ.New() to build a command from the Greeter's 20 | // struct fields. The resulting *writ.Command decodes and updates the 21 | // Greeter's fields in-place. The Command.ExitHelp() method is used to 22 | // display help content if --help is specified, or if invalid input 23 | // arguments are received. 24 | func Example_basic() { 25 | greeter := &Greeter{} 26 | cmd := writ.New("greeter", greeter) 27 | cmd.Help.Usage = "Usage: greeter [OPTION]... MESSAGE" 28 | cmd.Help.Header = "Greet users, displaying MESSAGE" 29 | 30 | // Use cmd.Decode(os.Args[1:]) in a real application 31 | _, positional, err := cmd.Decode([]string{"-vvv", "--name", "Sam", "How's it going?"}) 32 | if err != nil || greeter.HelpFlag { 33 | cmd.ExitHelp(err) 34 | } 35 | 36 | message := strings.Join(positional, " ") 37 | fmt.Printf("Hi %s! %s\n", greeter.Name, message) 38 | if greeter.Verbosity > 0 { 39 | fmt.Printf("I'm feeling re%slly chatty today!\n", strings.Repeat("a", greeter.Verbosity)) 40 | } 41 | 42 | // Output: 43 | // Hi Sam! How's it going? 44 | // I'm feeling reaaally chatty today! 45 | 46 | // Help Output: 47 | // Usage: greeter [OPTION]... MESSAGE 48 | // Greet users, displaying MESSAGE 49 | // 50 | // Available Options: 51 | // --help Display this help message and exit 52 | // -v, --verbose Display verbose output 53 | // -n, --name=ARG The person or people to greet 54 | } 55 | -------------------------------------------------------------------------------- /example_convenience_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Bob Ziuchkovski. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package writ_test 6 | 7 | import ( 8 | "bufio" 9 | "errors" 10 | "fmt" 11 | "github.com/bobziuchkovski/writ" 12 | "io" 13 | "os" 14 | "strings" 15 | ) 16 | 17 | type ReplacerCmd struct { 18 | Input io.Reader `option:"i" description:"Read input values from FILE (default: stdin)" default:"-" placeholder:"FILE"` 19 | Output io.WriteCloser `option:"o" description:"Write output to FILE (default: stdout)" default:"-" placeholder:"FILE"` 20 | Replacements map[string]string `option:"r, replace" description:"Replace occurrences of ORIG with NEW" placeholder:"ORIG=NEW"` 21 | HelpFlag bool `flag:"h, help" description:"Display this help text and exit"` 22 | } 23 | 24 | // This example demonstrates some of the convenience features offered by writ. 25 | // It uses writ's support for io types and default values to ensure the Input 26 | // and Output fields are initialized. These default to stdin and stdout due 27 | // to the default:"-" field tags. The user may specify -i or -o to read from 28 | // or write to a file. Similarly, the Replacements map is initialized with 29 | // key=value pairs for every -r/--replace option the user specifies. 30 | func Example_convenience() { 31 | replacer := &ReplacerCmd{} 32 | cmd := writ.New("replacer", replacer) 33 | cmd.Help.Usage = "Usage: replacer [OPTION]..." 34 | cmd.Help.Header = "Perform text replacement according to the -r/--replace option" 35 | cmd.Help.Footer = "By default, replacer reads from stdin and write to stdout. Use the -i and -o options to override." 36 | 37 | // Decode input arguments 38 | _, positional, err := cmd.Decode(os.Args[1:]) 39 | if err != nil || replacer.HelpFlag { 40 | cmd.ExitHelp(err) 41 | } 42 | if len(positional) > 0 { 43 | cmd.ExitHelp(errors.New("replacer does not accept positional arguments")) 44 | } 45 | 46 | // At this point, the ReplacerCmd's Input, Output, and Replacements fields are all 47 | // known-valid and initialized, so we can run the replacement. 48 | err = replacer.Replace() 49 | if err != nil { 50 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 51 | os.Exit(1) 52 | } 53 | } 54 | 55 | // The Replace() method performs the input/output replacements, but is 56 | // not relevant to the example itself. 57 | func (r ReplacerCmd) Replace() error { 58 | var pairs []string 59 | for k, v := range r.Replacements { 60 | pairs = append(pairs, k, v) 61 | } 62 | replacer := strings.NewReplacer(pairs...) 63 | scanner := bufio.NewScanner(r.Input) 64 | for scanner.Scan() { 65 | line := scanner.Text() 66 | _, err := io.WriteString(r.Output, replacer.Replace(line)+"\n") 67 | if err != nil { 68 | return err 69 | } 70 | } 71 | err := scanner.Err() 72 | if err != nil { 73 | return err 74 | } 75 | return r.Output.Close() 76 | 77 | // Help Output: 78 | // Usage: replacer [OPTION]... 79 | // Perform text replacement according to the -r/--replace option 80 | // 81 | // Available Options: 82 | // -i FILE Read input values from FILE (default: stdin) 83 | // -o FILE Write output to FILE (default: stdout) 84 | // -r, --replace=ORIG=NEW Replace occurrences of ORIG with NEW 85 | // -h, --help Display this help text and exit 86 | // 87 | // By default, replacer reads from stdin and write to stdout. Use the -i and -o options to override. 88 | } 89 | -------------------------------------------------------------------------------- /example_explicit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Bob Ziuchkovski. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package writ_test 6 | 7 | import ( 8 | "github.com/bobziuchkovski/writ" 9 | "os" 10 | "runtime" 11 | ) 12 | 13 | type Config struct { 14 | help bool 15 | verbosity int 16 | bootloader string 17 | } 18 | 19 | // This example demonstrates explicit Command and Option creation, 20 | // along with explicit option grouping. It checks the host platform 21 | // and dynamically adds a --bootloader option if the example is run on 22 | // Linux. The same result could be achieved by using writ.New() to 23 | // construct a Command, and then adding the platform-specific option 24 | // to the resulting Command directly. 25 | func Example_explicit() { 26 | config := &Config{} 27 | cmd := &writ.Command{Name: "explicit"} 28 | cmd.Help.Usage = "Usage: explicit [OPTION]... [ARG]..." 29 | cmd.Options = []*writ.Option{ 30 | { 31 | Names: []string{"h", "help"}, 32 | Description: "Display this help text and exit", 33 | Decoder: writ.NewFlagDecoder(&config.help), 34 | Flag: true, 35 | }, 36 | { 37 | Names: []string{"v"}, 38 | Description: "Increase verbosity; may be specified more than once", 39 | Decoder: writ.NewFlagAccumulator(&config.verbosity), 40 | Flag: true, 41 | Plural: true, 42 | }, 43 | } 44 | 45 | // Note the explicit option grouping. Using writ.New(), a single option group is 46 | // created for all options/flags that have descriptions. Without writ.New(), we 47 | // need to create the OptionGroup(s) ourselves. 48 | general := cmd.GroupOptions("help", "v") 49 | general.Header = "General Options:" 50 | cmd.Help.OptionGroups = append(cmd.Help.OptionGroups, general) 51 | 52 | // Dynamically add --bootloader on Linux 53 | if runtime.GOOS == "linux" { 54 | cmd.Options = append(cmd.Options, &writ.Option{ 55 | Names: []string{"bootloader"}, 56 | Description: "Use the specified bootloader (grub, grub2, or lilo)", 57 | Decoder: writ.NewOptionDecoder(&config.bootloader), 58 | Placeholder: "NAME", 59 | }) 60 | platform := cmd.GroupOptions("bootloader") 61 | platform.Header = "Platform Options:" 62 | cmd.Help.OptionGroups = append(cmd.Help.OptionGroups, platform) 63 | } 64 | 65 | // Decode the options 66 | _, _, err := cmd.Decode(os.Args[1:]) 67 | if err != nil || config.help { 68 | cmd.ExitHelp(err) 69 | } 70 | 71 | // Help Output, Linux: 72 | // General Options: 73 | // -h, --help Display this help text and exit 74 | // -v Increase verbosity; may be specified more than once 75 | // 76 | // Platform Options: 77 | // --bootloader=NAME Use the specified bootloader (grub, grub2, or lilo) 78 | // 79 | // Help Output, other platforms: 80 | // General Options: 81 | // -h, --help Display this help text and exit 82 | // -v Increase verbosity; may be specified more than once 83 | 84 | } 85 | -------------------------------------------------------------------------------- /example_subcommand_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Bob Ziuchkovski. All rights reserved. 2 | // Use of this source code is governed by a MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package writ_test 6 | 7 | import ( 8 | "errors" 9 | "github.com/bobziuchkovski/writ" 10 | "os" 11 | ) 12 | 13 | type GoBox struct { 14 | Link Link `command:"ln" alias:"link" description:"Create a soft or hard link"` 15 | List List `command:"ls" alias:"list" description:"List directory contents"` 16 | } 17 | 18 | type Link struct { 19 | HelpFlag bool `flag:"h, help" description:"Display this message and exit"` 20 | Symlink bool `flag:"s" description:"Create a symlink instead of a hard link"` 21 | } 22 | 23 | type List struct { 24 | HelpFlag bool `flag:"h, help" description:"Display this message and exit"` 25 | LongFormat bool `flag:"l" description:"Use long-format output"` 26 | } 27 | 28 | func (g *GoBox) Run(p writ.Path, positional []string) { 29 | // The user didn't specify a subcommand. Give them help. 30 | p.Last().ExitHelp(errors.New("COMMAND is required")) 31 | } 32 | 33 | func (l *Link) Run(p writ.Path, positional []string) { 34 | if l.HelpFlag { 35 | p.Last().ExitHelp(nil) 36 | } 37 | if len(positional) != 2 { 38 | p.Last().ExitHelp(errors.New("ln requires two arguments, OLD and NEW")) 39 | } 40 | // Link operation omitted for brevity. This would be os.Link or os.Symlink 41 | // based on the l.Symlink value. 42 | } 43 | 44 | func (l *List) Run(p writ.Path, positional []string) { 45 | if l.HelpFlag { 46 | p.Last().ExitHelp(nil) 47 | } 48 | // Listing operation omitted for brevity. This would be a call to ioutil.ReadDir 49 | // followed by conditional formatting based on the l.LongFormat value. 50 | } 51 | 52 | // This example demonstrates subcommands in a busybox style. There's no requirement 53 | // that subcommands implement the Run() method shown here. It's just an example of 54 | // how subcommands might be implemented. 55 | func Example_subcommand() { 56 | gobox := &GoBox{} 57 | cmd := writ.New("gobox", gobox) 58 | cmd.Help.Usage = "Usage: gobox COMMAND [OPTION]... [ARG]..." 59 | cmd.Subcommand("ln").Help.Usage = "Usage: gobox ln [-s] OLD NEW" 60 | cmd.Subcommand("ls").Help.Usage = "Usage: gobox ls [-l] [PATH]..." 61 | 62 | path, positional, err := cmd.Decode(os.Args[1:]) 63 | if err != nil { 64 | // Using path.Last() here ensures the user sees relevant help for their 65 | // command selection 66 | path.Last().ExitHelp(err) 67 | } 68 | 69 | // At this point, cmd.Decode() has already decoded option values into the gobox 70 | // struct, including subcommand values. We just need to dispatch the command. 71 | // path.String() is guaranteed to represent the user command selection. 72 | switch path.String() { 73 | case "gobox": 74 | gobox.Run(path, positional) 75 | case "gobox ln": 76 | gobox.Link.Run(path, positional) 77 | case "gobox ls": 78 | gobox.List.Run(path, positional) 79 | default: 80 | panic("BUG: Someone added a new command and forgot to add it's path here") 81 | } 82 | 83 | // Help output, gobox: 84 | // Usage: gobox COMMAND [OPTION]... [ARG]... 85 | // 86 | // Available Commands: 87 | // ln Create a soft or hard link 88 | // ls List directory contents 89 | // 90 | // Help output, gobox ln: 91 | // Usage: gobox ln [-s] OLD NEW 92 | // 93 | // Available Options: 94 | // -h, --help Display this message and exit 95 | // -s Create a symlink instead of a hard link 96 | // 97 | // Help output, gobox ls: 98 | // Usage: gobox ls [-l] [PATH]... 99 | // 100 | // Available Options: 101 | // -h, --help Display this message and exit 102 | // -l Use long-format output 103 | } 104 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package writ 22 | 23 | import ( 24 | "bytes" 25 | "fmt" 26 | "strings" 27 | "text/template" 28 | ) 29 | 30 | var templateFuncs = map[string]interface{}{ 31 | "formatCommand": formatCommand, 32 | "formatOption": formatOption, 33 | "wrapText": wrapText, 34 | } 35 | 36 | // The Help type is used for presentation purposes only, and does not affect 37 | // argument parsing. 38 | // 39 | // The Command.ExitHelp() and Command.WriteHelp() methods execute the 40 | // template assigned to the Template field, passing the Command as input. 41 | // If the Template field is nil, the writ package's default template is used. 42 | type Help struct { 43 | OptionGroups []OptionGroup 44 | CommandGroups []CommandGroup 45 | 46 | // Optional 47 | Template *template.Template // Used to render output 48 | Usage string // Short message displayed at the top of output 49 | Header string // Displayed after Usage 50 | Footer string // Displayed at the end of output 51 | } 52 | 53 | // OptionGroup is used to customize help output. It groups related Options 54 | // for output. When New() parses an input spec, it creates a single OptionGroup 55 | // for all parsed options that have descriptions. 56 | type OptionGroup struct { 57 | Options []*Option 58 | 59 | // Optional 60 | Name string // Not displayed; for matching purposes within the template 61 | Header string // Displayed before the group 62 | Footer string // Displayed after the group 63 | } 64 | 65 | // CommandGroup is used to customize help output. It groups related Commands 66 | // for output. When New() parses an input spec, it creates a single CommandGroup 67 | // for all parsed commands that have descriptions. 68 | type CommandGroup struct { 69 | Commands []*Command 70 | 71 | // Optional 72 | Name string // Not displayed; for matching purposes within the template 73 | Header string // Displayed before the group 74 | Footer string // Displayed after the group 75 | } 76 | 77 | func formatOption(o *Option) string { 78 | var placeholder string 79 | if !o.Flag { 80 | placeholder = o.Placeholder 81 | if placeholder == "" { 82 | placeholder = "ARG" 83 | } 84 | } 85 | names := "" 86 | short := o.ShortNames() 87 | long := o.LongNames() 88 | for i, s := range short { 89 | names += "-" + s 90 | if (i < len(short)-1) || len(long) != 0 { 91 | names += ", " 92 | } 93 | } 94 | if len(long) == 0 && placeholder != "" { 95 | names += " " + placeholder 96 | } 97 | for i, l := range long { 98 | names += "--" + l 99 | if i < len(long)-1 { 100 | names += ", " 101 | } else if placeholder != "" { 102 | names += "=" + placeholder 103 | } 104 | } 105 | 106 | formatted := fmt.Sprintf(" %-24s %s", names, o.Description) 107 | return wrapText(formatted, 80, 28) 108 | } 109 | 110 | func formatCommand(c *Command) string { 111 | formatted := fmt.Sprintf(" %-24s %s", c.Name, c.Description) 112 | return wrapText(formatted, 80, 28) 113 | } 114 | 115 | // This is a pretty naiive implementation, but it's late and I'm tired 116 | // TODO: cleanup and probably try to wrap on nearest space or punctuation 117 | func wrapText(s string, width int, indent int) string { 118 | buf := bytes.NewBuffer(nil) 119 | runes := []rune(s) 120 | linelen, i := 0, 0 121 | for i < len(runes) { 122 | if runes[i] == '\n' { 123 | buf.WriteString("\n") 124 | if i < len(runes) { 125 | buf.WriteString(strings.Repeat(" ", indent)) 126 | linelen = indent 127 | } 128 | } else if linelen == width { 129 | buf.WriteString("\n") 130 | if i < len(runes) { 131 | buf.WriteString(strings.Repeat(" ", indent)) 132 | linelen = indent 133 | } 134 | buf.WriteRune(runes[i]) 135 | } else { 136 | buf.WriteRune(runes[i]) 137 | } 138 | i++ 139 | linelen++ 140 | } 141 | return buf.String() 142 | } 143 | -------------------------------------------------------------------------------- /help_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package writ 22 | 23 | import ( 24 | "bytes" 25 | "io/ioutil" 26 | "testing" 27 | "text/template" 28 | ) 29 | 30 | var helpFormattingTests = []struct { 31 | Description string 32 | Spec interface{} 33 | Rendered string 34 | }{ 35 | { 36 | Description: "A single option", 37 | Spec: &struct { 38 | Flag bool `flag:"h, help" description:"Display this text and exit"` 39 | }{}, 40 | Rendered: `Usage: test [OPTION]... [ARG]... 41 | 42 | Available Options: 43 | -h, --help Display this text and exit 44 | `, 45 | }, 46 | 47 | { 48 | Description: "A couple options", 49 | Spec: &struct { 50 | Flag bool `flag:"h" description:"Display this text and exit"` 51 | Option int `option:"i, int" description:"An int option" placeholder:"INT"` 52 | }{}, 53 | Rendered: `Usage: test [OPTION]... [ARG]... 54 | 55 | Available Options: 56 | -h Display this text and exit 57 | -i, --int=INT An int option 58 | `, 59 | }, 60 | 61 | { 62 | Description: "Multiple long and short names for an option", 63 | Spec: &struct { 64 | Option int `option:"i, I, int, Int" description:"An int option" placeholder:"INT"` 65 | }{}, 66 | Rendered: `Usage: test [OPTION]... [ARG]... 67 | 68 | Available Options: 69 | -i, -I, --int, --Int=INT An int option 70 | `, 71 | }, 72 | 73 | { 74 | Description: "An option with short-form placeholder", 75 | Spec: &struct { 76 | Option int `option:"i" description:"An int option" placeholder:"INT"` 77 | }{}, 78 | Rendered: `Usage: test [OPTION]... [ARG]... 79 | 80 | Available Options: 81 | -i INT An int option 82 | `, 83 | }, 84 | 85 | { 86 | Description: "A single command", 87 | Spec: &struct { 88 | Command struct{} `command:"command" description:"A command"` 89 | }{}, 90 | Rendered: `Usage: test [OPTION]... [ARG]... 91 | 92 | Available Commands: 93 | command A command 94 | `, 95 | }, 96 | 97 | { 98 | Description: "A single option and single command", 99 | Spec: &struct { 100 | Option int `option:"i" description:"An int option" placeholder:"INT"` 101 | Command struct{} `command:"command" description:"A command"` 102 | }{}, 103 | Rendered: `Usage: test [OPTION]... [ARG]... 104 | 105 | Available Options: 106 | -i INT An int option 107 | 108 | Available Commands: 109 | command A command 110 | `, 111 | }, 112 | 113 | { 114 | Description: "Command description wrapping", 115 | Spec: &struct { 116 | Command struct{} `command:"command" description:"A command with a reeeeeeeeeeeeeeeeeeeeeeeeeeeeeaaaaaaaaaallllllyyyyy loooooooooooooooonnnnnnngggggg description"` 117 | }{}, 118 | Rendered: `Usage: test [OPTION]... [ARG]... 119 | 120 | Available Commands: 121 | command A command with a reeeeeeeeeeeeeeeeeeeeeeeeeeeeeaaaaa 122 | aaaaallllllyyyyy loooooooooooooooonnnnnnngggggg desc 123 | ription 124 | `, 125 | }, 126 | 127 | { 128 | Description: "Command description wrapping with explicit newline in description", 129 | Spec: &struct { 130 | Command struct{} `command:"command" description:"A command with a\nnew line in the description"` 131 | }{}, 132 | Rendered: `Usage: test [OPTION]... [ARG]... 133 | 134 | Available Commands: 135 | command A command with a 136 | new line in the description 137 | `, 138 | }, 139 | 140 | { 141 | Description: "Option description wrapping", 142 | Spec: &struct { 143 | Option int `option:"opt" description:"An option with a reeeeeeeeeeeeeeeeeeeeeeeeeeeeeaaaaaaaaaallllllyyyyy loooooooooooooooonnnnnnngggggg description"` 144 | }{}, 145 | Rendered: `Usage: test [OPTION]... [ARG]... 146 | 147 | Available Options: 148 | --opt=ARG An option with a reeeeeeeeeeeeeeeeeeeeeeeeeeeeeaaaaa 149 | aaaaallllllyyyyy loooooooooooooooonnnnnnngggggg desc 150 | ription 151 | `, 152 | }, 153 | 154 | { 155 | Description: "Option description wrapping with explicit newline in description", 156 | Spec: &struct { 157 | Option int `option:"opt" description:"An option with a\nnew line in the description"` 158 | }{}, 159 | Rendered: `Usage: test [OPTION]... [ARG]... 160 | 161 | Available Options: 162 | --opt=ARG An option with a 163 | new line in the description 164 | `, 165 | }, 166 | 167 | { 168 | Description: "Hidden option", 169 | Spec: &struct { 170 | Hidden int `option:"hidden"` 171 | Flag bool `flag:"h, help" description:"Display this text and exit"` 172 | }{}, 173 | Rendered: `Usage: test [OPTION]... [ARG]... 174 | 175 | Available Options: 176 | -h, --help Display this text and exit 177 | `, 178 | }, 179 | 180 | { 181 | Description: "Hidden command", 182 | Spec: &struct { 183 | Command struct{} `command:"command" description:"A command"` 184 | Hidden struct{} `command:"hidden"` 185 | }{}, 186 | Rendered: `Usage: test [OPTION]... [ARG]... 187 | 188 | Available Commands: 189 | command A command 190 | `, 191 | }, 192 | } 193 | 194 | func TestHelpFormatting(t *testing.T) { 195 | for _, test := range helpFormattingTests { 196 | cmd := New("test", test.Spec) 197 | buf := bytes.NewBuffer(nil) 198 | err := cmd.WriteHelp(buf) 199 | if err != nil { 200 | t.Errorf("Encountered unexpecting error running test. Description: %s, Error: %s", test.Description, err) 201 | continue 202 | } 203 | if buf.String() != test.Rendered { 204 | t.Errorf("\nHelp output invalid. Test Description: %s\n===Expected===\n%s\n\n===Received:===\n%s", test.Description, test.Rendered, buf.String()) 205 | continue 206 | } 207 | } 208 | } 209 | 210 | func TestCustomHelpTemplate(t *testing.T) { 211 | templateText := "Custom content!" 212 | tpl := template.Must(template.New("Help").Parse(templateText)) 213 | cmd := New("test", &struct{}{}) 214 | cmd.Help.Template = tpl 215 | buf := bytes.NewBuffer(nil) 216 | err := cmd.WriteHelp(buf) 217 | if err != nil { 218 | t.Errorf("Encountered unexpecting error running custom template test. Error: %s", err) 219 | return 220 | } 221 | if buf.String() != templateText { 222 | t.Errorf("Custom help output invalid. Expected: %q, Received: %q", templateText, buf.String()) 223 | return 224 | } 225 | } 226 | 227 | func TestInvalidHelpTemplate(t *testing.T) { 228 | templateText := "{{.Bogus}}" 229 | tpl := template.Must(template.New("Help").Parse(templateText)) 230 | cmd := New("test", &struct{}{}) 231 | cmd.Help.Template = tpl 232 | 233 | defer func() { 234 | r := recover() 235 | if r != nil { 236 | switch r.(type) { 237 | case commandError, optionError: 238 | // Intentionally blank 239 | default: 240 | panic(r) 241 | } 242 | } 243 | }() 244 | cmd.WriteHelp(ioutil.Discard) 245 | t.Errorf("Expected cmd.WriteHelp() to panic on invalid template, but this didn't happen") 246 | } 247 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package writ 22 | 23 | import ( 24 | "fmt" 25 | "io" 26 | "os" 27 | "reflect" 28 | "strconv" 29 | "strings" 30 | "unicode" 31 | ) 32 | 33 | var ( 34 | readerPtr *io.Reader 35 | readCloserPtr *io.ReadCloser 36 | writerPtr *io.Writer 37 | writeCloserPtr *io.WriteCloser 38 | readerT = reflect.TypeOf(readerPtr).Elem() 39 | readCloserT = reflect.TypeOf(readCloserPtr).Elem() 40 | writerT = reflect.TypeOf(writerPtr).Elem() 41 | writeCloserT = reflect.TypeOf(writeCloserPtr).Elem() 42 | ) 43 | 44 | type optionError struct { 45 | err error 46 | } 47 | 48 | func (e optionError) Error() string { 49 | return e.err.Error() 50 | } 51 | 52 | // panicOption reports invalid use of the Option type 53 | func panicOption(format string, values ...interface{}) { 54 | e := optionError{fmt.Errorf(format, values...)} 55 | panic(e) 56 | } 57 | 58 | // Option specifies program options and flags. 59 | type Option struct { 60 | // Required 61 | Names []string 62 | Decoder OptionDecoder 63 | 64 | // Optional 65 | Flag bool // If set, the Option takes no arguments 66 | Plural bool // If set, the Option may be specified multiple times 67 | Description string // Options without descriptions are hidden 68 | Placeholder string // Displayed next to option in help output (e.g. FILE) 69 | } 70 | 71 | // ShortNames returns a filtered slice of the names that are exactly one rune in length. 72 | func (o *Option) ShortNames() []string { 73 | var short []string 74 | for _, n := range o.Names { 75 | if len([]rune(n)) == 1 { 76 | short = append(short, n) 77 | } 78 | } 79 | return short 80 | } 81 | 82 | // LongNames returns a filtered slice of the names that are longer than one rune in length. 83 | func (o *Option) LongNames() []string { 84 | var long []string 85 | for _, n := range o.Names { 86 | if len([]rune(n)) > 1 { 87 | long = append(long, n) 88 | } 89 | } 90 | return long 91 | } 92 | 93 | func (o *Option) String() string { 94 | var short, long []string 95 | for _, s := range o.ShortNames() { 96 | short = append(short, "-"+s) 97 | } 98 | for _, l := range o.LongNames() { 99 | long = append(long, "--"+l) 100 | } 101 | return strings.Join(append(short, long...), "/") 102 | } 103 | 104 | func (o *Option) validate() { 105 | if len(o.Names) == 0 { 106 | panicOption("Options require at least one name: %#v", o) 107 | } 108 | for _, name := range o.Names { 109 | if name == "" { 110 | panicOption("Option names cannot be blank: %#v", o) 111 | } 112 | if strings.HasPrefix(name, "-") { 113 | panicOption("Option names cannot begin with '-' (option %s)", name) 114 | } 115 | runes := []rune(name) 116 | for _, r := range runes { 117 | if unicode.IsSpace(r) { 118 | panicOption("Option names cannot have spaces (option %q)", name) 119 | } 120 | } 121 | } 122 | if o.Decoder == nil { 123 | panicOption("Option decoder cannot be nil (option %s)", o.String()) 124 | } 125 | } 126 | 127 | // OptionDecoder is used for decoding Option arguments. Every Option must 128 | // have an OptionDecoder assigned. New() constructs and assigns 129 | // OptionDecoders automatically for supported field types. 130 | type OptionDecoder interface { 131 | Decode(arg string) error 132 | } 133 | 134 | type decoderFunc func(rval reflect.Value, arg string) error 135 | 136 | func decodeInt(rval reflect.Value, arg string) error { 137 | v, err := strconv.ParseInt(arg, 10, 64) 138 | if err != nil { 139 | return err 140 | } 141 | if rval.OverflowInt(v) { 142 | return fmt.Errorf("value %d would overflow %s", v, rval.Kind()) 143 | } 144 | rval.Set(reflect.ValueOf(v).Convert(rval.Type())) 145 | return nil 146 | } 147 | 148 | func decodeUint(rval reflect.Value, arg string) error { 149 | v, err := strconv.ParseUint(arg, 10, 64) 150 | if err != nil { 151 | return err 152 | } 153 | if rval.OverflowUint(v) { 154 | return fmt.Errorf("value %d would overflow %s", v, rval.Kind()) 155 | } 156 | rval.Set(reflect.ValueOf(v).Convert(rval.Type())) 157 | return nil 158 | } 159 | 160 | func decodeFloat(rval reflect.Value, arg string) error { 161 | v, err := strconv.ParseFloat(arg, 64) 162 | if err != nil { 163 | return err 164 | } 165 | if rval.OverflowFloat(v) { 166 | return fmt.Errorf("value %f would overflow %s", v, rval.Kind()) 167 | } 168 | rval.Set(reflect.ValueOf(v).Convert(rval.Type())) 169 | return nil 170 | } 171 | 172 | func decodeString(rval reflect.Value, arg string) error { 173 | rval.Set(reflect.ValueOf(arg)) 174 | return nil 175 | } 176 | 177 | func getDecoderFunc(kind reflect.Kind) decoderFunc { 178 | switch kind { 179 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 180 | return decodeInt 181 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 182 | return decodeUint 183 | case reflect.Float32, reflect.Float64: 184 | return decodeFloat 185 | case reflect.String: 186 | return decodeString 187 | default: 188 | return nil 189 | } 190 | } 191 | 192 | // NewOptionDecoder builds an OptionDecoder for supported value types. The val 193 | // parameter must be a pointer to one of the following supported types: 194 | // 195 | // int, int8, int16, int32, int64, uint, uint8, iunt16, uint32, uint64 196 | // float32, float64 197 | // string, []string 198 | // map[string]string 199 | // Argument must be in key=value format. 200 | // io.Reader, io.ReadCloser 201 | // Argument must be a path to an existing file, or "-" to specify os.Stdin 202 | // io.Writer, io.WriteCloser 203 | // Argument will be used to create a new file, or "-" to specify os.Stdout. 204 | // If a file already exists at the path specified, it will be overwritten. 205 | func NewOptionDecoder(val interface{}) OptionDecoder { 206 | rval := reflect.ValueOf(val) 207 | if rval.Kind() != reflect.Ptr { 208 | panicOption("NewDecoder must be called on a pointer") 209 | } 210 | if rval.IsNil() { 211 | panicOption("NewDecoder called on nil pointer") 212 | } 213 | elem := rval.Elem() 214 | etype := elem.Type() 215 | ekind := elem.Kind() 216 | 217 | var decoder OptionDecoder 218 | if etype == readerT || etype == readCloserT { 219 | decoder = inputDecoder{elem} 220 | } else if etype == writerT || etype == writeCloserT { 221 | decoder = outputDecoder{elem} 222 | } else if ekind == reflect.Slice && etype.Elem().Kind() == reflect.String { 223 | decoder = stringSliceDecoder{rval.Interface().(*[]string)} 224 | } else if ekind == reflect.Map && etype.Key().Kind() == reflect.String && etype.Elem().Kind() == reflect.String { 225 | decoder = stringMapDecoder{rval.Interface().(*map[string]string)} 226 | } else { 227 | decoderFunc := getDecoderFunc(ekind) 228 | if decoderFunc != nil { 229 | decoder = basicDecoder{elem, decoderFunc} 230 | } 231 | } 232 | if decoder == nil { 233 | panicOption("no option decoder available for type %s", rval.Type()) 234 | } 235 | return decoder 236 | } 237 | 238 | type basicDecoder struct { 239 | rval reflect.Value 240 | decoderFunc decoderFunc 241 | } 242 | 243 | func (d basicDecoder) Decode(arg string) error { 244 | return d.decoderFunc(d.rval, arg) 245 | } 246 | 247 | type stringSliceDecoder struct { 248 | value *[]string 249 | } 250 | 251 | func (d stringSliceDecoder) Decode(arg string) error { 252 | *d.value = append(*d.value, arg) 253 | return nil 254 | } 255 | 256 | type stringMapDecoder struct { 257 | value *map[string]string 258 | } 259 | 260 | func (d stringMapDecoder) Decode(arg string) error { 261 | keyval := strings.SplitN(arg, "=", 2) 262 | if len(keyval) != 2 { 263 | return fmt.Errorf("argument %q is not in key=value format", arg) 264 | } 265 | if *d.value == nil { 266 | *d.value = make(map[string]string) 267 | } 268 | (*d.value)[keyval[0]] = keyval[1] 269 | return nil 270 | } 271 | 272 | type inputDecoder struct { 273 | rval reflect.Value 274 | } 275 | 276 | func (d inputDecoder) Decode(arg string) error { 277 | var err error 278 | var f *os.File 279 | if arg == "-" { 280 | f = os.Stdin 281 | } else { 282 | f, err = os.Open(arg) 283 | } 284 | if err != nil { 285 | return err 286 | } 287 | d.rval.Set(reflect.ValueOf(f).Convert(d.rval.Type())) 288 | return nil 289 | } 290 | 291 | type outputDecoder struct { 292 | rval reflect.Value 293 | } 294 | 295 | func (d outputDecoder) Decode(arg string) error { 296 | var err error 297 | var f *os.File 298 | if arg == "-" { 299 | f = os.Stdout 300 | } else { 301 | f, err = os.Create(arg) 302 | } 303 | if err != nil { 304 | return err 305 | } 306 | d.rval.Set(reflect.ValueOf(f).Convert(d.rval.Type())) 307 | return nil 308 | } 309 | 310 | func (d flagAccumulator) Decode(arg string) error { 311 | *d.value++ 312 | return nil 313 | } 314 | 315 | // NewFlagDecoder builds an OptionDecoder for boolean flag values. The boolean 316 | // value is set when the option is decoded. 317 | func NewFlagDecoder(val *bool) OptionDecoder { 318 | if val == nil { 319 | panicOption("NewFlagDecoder called with a nil pointer") 320 | } 321 | return flagDecoder{val} 322 | } 323 | 324 | type flagDecoder struct { 325 | value *bool 326 | } 327 | 328 | func (d flagDecoder) Decode(arg string) error { 329 | *d.value = true 330 | return nil 331 | } 332 | 333 | // NewFlagAccumulator builds an OptionDecoder for int flag values. The int value 334 | // is incremented every time the option is decoded. 335 | func NewFlagAccumulator(val *int) OptionDecoder { 336 | return flagAccumulator{val} 337 | } 338 | 339 | type flagAccumulator struct { 340 | value *int 341 | } 342 | 343 | // OptionDefaulter initializes option values to defaults. If an OptionDecoder 344 | // implements the OptionDefaulter interface, its SetDefault() method is called 345 | // prior to decoding options. 346 | type OptionDefaulter interface { 347 | SetDefault() 348 | } 349 | 350 | // NewDefaulter builds an OptionDecoder that implements OptionDefaulter. 351 | // SetDefault calls decoder.Decode() with the value of defaultArg. If the 352 | // value fails to decode, SetDefault panics. 353 | func NewDefaulter(decoder OptionDecoder, defaultArg string) OptionDecoder { 354 | return defaulter{decoder, defaultArg} 355 | } 356 | 357 | type defaulter struct { 358 | OptionDecoder 359 | defaultArg string 360 | } 361 | 362 | func (d defaulter) SetDefault() { 363 | err := d.Decode(d.defaultArg) 364 | if err != nil { 365 | // Default values should be known correct values, so we panic on error 366 | panicOption("error setting default value: decoder rejected arg %q", d.defaultArg) 367 | } 368 | } 369 | 370 | // NewEnvDefaulter builds an OptionDecoder that implements OptionDefaulter. 371 | // SetDefault calls decoder.Decode() with the value of the environment 372 | // variable named by key. If the environment variable isn't set or fails to 373 | // decode, SetDefault checks if decoder implements OptionDefault. If so, 374 | // SetDefault calls decoder.SetDefault(). Otherwise, no action is taken. 375 | func NewEnvDefaulter(decoder OptionDecoder, key string) OptionDecoder { 376 | return envDefaulter{decoder, key} 377 | } 378 | 379 | type envDefaulter struct { 380 | OptionDecoder 381 | key string 382 | } 383 | 384 | func (d envDefaulter) SetDefault() { 385 | val := os.Getenv(d.key) 386 | if val != "" { 387 | err := d.Decode(val) 388 | if err == nil { 389 | return 390 | } 391 | } 392 | 393 | defaulter, ok := d.OptionDecoder.(OptionDefaulter) 394 | if ok { 395 | defaulter.SetDefault() 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /option_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package writ 22 | 23 | import ( 24 | "fmt" 25 | "testing" 26 | ) 27 | 28 | /* 29 | * Much of the option testing occurs indirectly via command_test.go 30 | */ 31 | 32 | type noopDecoder struct{} 33 | 34 | func (d noopDecoder) Decode(arg string) error { return nil } 35 | 36 | var invalidOptionTests = []struct { 37 | Description string 38 | Option *Option 39 | }{ 40 | { 41 | Description: "Option must have a name 1", 42 | Option: &Option{Decoder: noopDecoder{}}, 43 | }, 44 | { 45 | Description: "Option must have a name 1", 46 | Option: &Option{Names: []string{}, Decoder: noopDecoder{}}, 47 | }, 48 | { 49 | Description: "Option names cannot be blank", 50 | Option: &Option{Names: []string{""}, Decoder: noopDecoder{}}, 51 | }, 52 | { 53 | Description: "Option names cannot begin with -", 54 | Option: &Option{Names: []string{"-option"}, Decoder: noopDecoder{}}, 55 | }, 56 | { 57 | Description: "Option names cannot have spaces 1", 58 | Option: &Option{Names: []string{" option"}, Decoder: noopDecoder{}}, 59 | }, 60 | { 61 | Description: "Option names cannot have spaces 2", 62 | Option: &Option{Names: []string{"option "}, Decoder: noopDecoder{}}, 63 | }, 64 | { 65 | Description: "Option names cannot have spaces 3", 66 | Option: &Option{Names: []string{"option spaces"}, Decoder: noopDecoder{}}, 67 | }, 68 | { 69 | Description: "Option must have a decoder", 70 | Option: &Option{Names: []string{"option"}}, 71 | }, 72 | } 73 | 74 | func TestDirectOptionValidation(t *testing.T) { 75 | for _, test := range invalidOptionTests { 76 | err := checkInvalidOption(test.Option) 77 | if err == nil { 78 | t.Errorf("Expected error validating option, but none received. Test: %s", test.Description) 79 | continue 80 | } 81 | } 82 | } 83 | 84 | func checkInvalidOption(opt *Option) (err error) { 85 | defer func() { 86 | r := recover() 87 | if r != nil { 88 | switch e := r.(type) { 89 | case commandError: 90 | err = e 91 | case optionError: 92 | err = e 93 | default: 94 | panic(e) 95 | } 96 | } 97 | }() 98 | opt.validate() 99 | return nil 100 | } 101 | 102 | func TestNilNewOptionDecoder(t *testing.T) { 103 | var nilptr *bool 104 | defer func() { 105 | r := recover() 106 | if r != nil { 107 | switch r.(type) { 108 | case commandError, optionError: 109 | // Intentionally blank 110 | default: 111 | panic(r) 112 | } 113 | } 114 | }() 115 | NewOptionDecoder(nilptr) 116 | t.Errorf("Expected NewOptionDecoder to panic on nil value, but this didn't happen") 117 | } 118 | 119 | func TestNonPointerNewOptionDecoder(t *testing.T) { 120 | val := true 121 | defer func() { 122 | r := recover() 123 | if r != nil { 124 | switch r.(type) { 125 | case commandError, optionError: 126 | // Intentionally blank 127 | default: 128 | panic(r) 129 | } 130 | } 131 | }() 132 | NewOptionDecoder(val) 133 | t.Errorf("Expected NewOptionDecoder to panic on non-pointer type, but this didn't happen") 134 | } 135 | 136 | func TestNilNewFlagDecoder(t *testing.T) { 137 | var nilptr *bool 138 | defer func() { 139 | r := recover() 140 | if r != nil { 141 | switch r.(type) { 142 | case commandError, optionError: 143 | // Intentionally blank 144 | default: 145 | panic(r) 146 | } 147 | } 148 | }() 149 | NewFlagDecoder(nilptr) 150 | t.Errorf("Expected NewFlagDecoder to panic on nil value, but this didn't happen") 151 | } 152 | 153 | /* 154 | * Misc coverage tests to ensure code doesn't panic 155 | */ 156 | 157 | func TestOptionError(t *testing.T) { 158 | err := optionError{fmt.Errorf("test")} 159 | if err.Error() != "test" { 160 | t.Errorf("Expected optionError to return underlying error string. Expected: %q, Received: %q", "test", err.Error()) 161 | } 162 | } 163 | 164 | // Ensure Option.String() doesn't panic. We make no guarantee 165 | // on the output formatting. 166 | func TestOptionString(t *testing.T) { 167 | opt := &Option{Names: []string{"o", "O", "opt", "Opt"}} 168 | if opt.String() == "" { 169 | t.Errorf("Option.String() returned an empty string") 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | // +build go1.6 2 | 3 | // Copyright (c) 2016 Bob Ziuchkovski 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | package writ 24 | 25 | import ( 26 | "text/template" 27 | ) 28 | 29 | var defaultTemplate = template.Must(template.New("Help").Funcs(templateFuncs).Parse(HelpText)) 30 | 31 | // HelpText is used by Command.WriteHelp() and Command.ExitHelp() to generate 32 | // help content. 33 | // 34 | // This copy of the template is used when compiling with go 1.6+. 35 | // See template_legacy.go for the go < 1.6 template. 36 | // Output is the same for both templates, but this one is easier to read. 37 | const HelpText = `{{/**/ -}} 38 | 39 | {{block "Main" .}}{{end -}} 40 | 41 | {{define "Main" -}} 42 | {{block "Usage" .}}{{end -}} 43 | {{block "Header" .}}{{end -}} 44 | {{block "Body" .}}{{end -}} 45 | {{block "Footer" .}}{{end -}} 46 | {{end -}} 47 | 48 | {{define "Usage" -}} 49 | {{with .Help.Usage -}}{{.}}{{"\n"}}{{end -}} 50 | {{end -}} 51 | 52 | {{define "Header"}}{{with .Help.Header}}{{.}}{{"\n"}}{{end}}{{end -}} 53 | 54 | {{define "Body" -}} 55 | {{block "OptionGroups" .}}{{end -}} 56 | {{block "CommandGroups" .}}{{end -}} 57 | {{end -}} 58 | 59 | {{define "OptionGroups" -}} 60 | {{with .Help.OptionGroups -}} 61 | {{range .}}{{block "OptionGroup" .}}{{end}}{{end -}} 62 | {{end -}} 63 | {{end -}} 64 | 65 | {{define "OptionGroup" -}} 66 | {{"\n" -}} 67 | {{with .Header}}{{.}}{{"\n"}}{{end -}} 68 | {{with .Options -}} 69 | {{range .}}{{block "OptionHelp" .}}{{end}}{{end -}} 70 | {{end -}} 71 | {{with .Footer}}{{.}}{{"\n"}}{{end -}} 72 | {{end -}} 73 | 74 | {{define "OptionHelp"}}{{formatOption .}}{{"\n"}}{{end -}} 75 | 76 | {{define "CommandGroups" -}} 77 | {{with .Help.CommandGroups -}} 78 | {{range .}}{{block "CommandGroup" .}}{{end}}{{end -}} 79 | {{end -}} 80 | {{end -}} 81 | 82 | {{define "CommandGroup" -}} 83 | {{"\n" -}} 84 | {{with .Header}}{{.}}{{"\n"}}{{end -}} 85 | {{with .Commands -}} 86 | {{range .}}{{block "CommandHelp" .}}{{end}}{{end -}} 87 | {{end -}} 88 | {{with .Footer}}{{.}}{{"\n"}}{{end -}} 89 | {{end -}} 90 | 91 | {{define "CommandHelp"}}{{formatCommand .}}{{"\n"}}{{end -}} 92 | 93 | {{define "Footer"}}{{with .Help.Footer}}{{"\n"}}{{.}}{{"\n"}}{{end}}{{end -}} 94 | ` 95 | -------------------------------------------------------------------------------- /template_legacy.go: -------------------------------------------------------------------------------- 1 | // +build !go1.6 2 | 3 | // Copyright (c) 2016 Bob Ziuchkovski 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | package writ 24 | 25 | import ( 26 | "text/template" 27 | ) 28 | 29 | var defaultTemplate = template.Must(template.New("Help").Funcs(templateFuncs).Parse(HelpText)) 30 | 31 | // HelpText is used by Command.WriteHelp() and Command.ExitHelp() to generate 32 | // help content. 33 | // 34 | // This copy of the template is used when compiling with go < 1.6. 35 | // It's a bit ugly due to the lack of whitespace control with Go < 1.6. 36 | // See template.go for the go1.6+ help template. 37 | const HelpText = `{{/* 38 | */}}{{template "Main" .}}{{/* 39 | 40 | */}}{{define "Main"}}{{/* 41 | */}}{{template "Usage" .}}{{/* 42 | */}}{{template "Header" .}}{{/* 43 | */}}{{template "Body" .}}{{/* 44 | */}}{{template "Footer" .}}{{/* 45 | */}}{{end}}{{/* 46 | 47 | */}}{{define "Usage"}}{{/* 48 | */}}{{with .Help.Usage}}{{.}}{{"\n"}}{{end}}{{/* 49 | */}}{{end}}{{/* 50 | 51 | */}}{{define "Header"}}{{with .Help.Header}}{{.}}{{"\n"}}{{end}}{{end}}{{/* 52 | 53 | */}}{{define "Body"}}{{/* 54 | */}}{{template "OptionGroups" .}}{{/* 55 | */}}{{template "CommandGroups" .}}{{/* 56 | */}}{{end}}{{/* 57 | 58 | */}}{{define "OptionGroups"}}{{/* 59 | */}}{{with .Help.OptionGroups}}{{/* 60 | */}}{{range .}}{{template "OptionGroup" .}}{{end}}{{/* 61 | */}}{{end}}{{/* 62 | */}}{{end}}{{/* 63 | 64 | */}}{{define "OptionGroup"}}{{/* 65 | */}}{{"\n"}}{{/* 66 | */}}{{with .Header}}{{.}}{{"\n"}}{{end}}{{/* 67 | */}}{{with .Options}}{{/* 68 | */}}{{range .}}{{template "OptionHelp" .}}{{end}}{{/* 69 | */}}{{end}}{{/* 70 | */}}{{with .Footer}}{{.}}{{"\n"}}{{end}}{{/* 71 | */}}{{end}}{{/* 72 | 73 | */}}{{define "OptionHelp"}}{{formatOption .}}{{"\n"}}{{end}}{{/* 74 | 75 | */}}{{define "CommandGroups"}}{{/* 76 | */}}{{with .Help.CommandGroups}}{{/* 77 | */}}{{range .}}{{template "CommandGroup" .}}{{end}}{{/* 78 | */}}{{end}}{{/* 79 | */}}{{end}}{{/* 80 | 81 | */}}{{define "CommandGroup"}}{{/* 82 | */}}{{"\n"}}{{/* 83 | */}}{{with .Header}}{{.}}{{"\n"}}{{end}}{{/* 84 | */}}{{with .Commands}}{{/* 85 | */}}{{range .}}{{template "CommandHelp" .}}{{end}}{{/* 86 | */}}{{end}}{{/* 87 | */}}{{with .Footer}}{{.}}{{"\n"}}{{end}}{{/* 88 | */}}{{end}}{{/* 89 | 90 | */}}{{define "CommandHelp"}}{{formatCommand .}}{{"\n"}}{{end}}{{/* 91 | 92 | */}}{{define "Footer"}}{{with .Help.Footer}}{{"\n"}}{{.}}{{"\n"}}{{end}}{{end}}` 93 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Bob Ziuchkovski 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package writ 22 | 23 | // Version records the writ package version. 24 | var Version = struct { 25 | Major int 26 | Minor int 27 | Patch int 28 | }{0, 8, 9} 29 | --------------------------------------------------------------------------------