├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── doc.go ├── go.mod ├── go.sum ├── item.go ├── node.go ├── node_build.go ├── node_commands.go ├── node_complete.go ├── node_help.go ├── node_parse.go ├── opts.go ├── opts_test.go ├── strings.go └── strings_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: CI 3 | jobs: 4 | # ================ 5 | # TEST JOB 6 | # runs on every push and PR 7 | # ================ 8 | test: 9 | name: Test 10 | strategy: 11 | matrix: 12 | go-version: [1.17.x, 1.18.x, 1.19.x] 13 | platform: [ubuntu-latest, macos-latest, windows-latest] 14 | runs-on: ${{ matrix.platform }} 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | - name: Build 23 | run: go build -v -o /dev/null 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Jaime Pillora 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo
3 | A Go (golang) package for building frictionless command-line interfaces

4 | 5 | GoDoc 6 | 7 | 8 | CI 9 | 10 |

11 | 12 | --- 13 | 14 | Creating command-line interfaces should be simple: 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "log" 21 | 22 | "github.com/jpillora/opts" 23 | ) 24 | 25 | func main() { 26 | type config struct { 27 | File string `opts:"help=file to load"` 28 | Lines int `opts:"help=number of lines to show"` 29 | } 30 | c := config{} 31 | opts.Parse(&c) 32 | log.Printf("%+v", c) 33 | } 34 | ``` 35 | 36 | ```sh 37 | $ go build -o my-prog 38 | $ ./my-prog --help 39 | 40 | Usage: my-prog [options] 41 | 42 | Options: 43 | --file, -f file to load 44 | --lines, -l number of lines to show 45 | --help, -h display help 46 | 47 | ``` 48 | 49 | ```sh 50 | $ ./my-prog -f foo.txt -l 42 51 | {File:foo.txt Lines:42} 52 | ``` 53 | 54 | *Try it out https://play.golang.org/p/D0jWFwmxRgt* 55 | 56 | ### Features (with examples) 57 | 58 | - Easy to use ([eg-helloworld](https://github.com/jpillora/opts-examples/tree/master/eg-helloworld/)) 59 | - Promotes separation of CLI code and library code ([eg-app](https://github.com/jpillora/opts-examples/tree/master/eg-app/)) 60 | - Automatically generated `--help` text via struct tags ([eg-help](https://github.com/jpillora/opts-examples/tree/master/eg-help/)) 61 | - Default values by modifying the struct prior to `Parse()` ([eg-defaults](https://github.com/jpillora/opts-examples/tree/master/eg-defaults/)) 62 | - Default values from a JSON config file, unmarshalled via your config struct ([eg-config](https://github.com/jpillora/opts-examples/tree/master/eg-config/)) 63 | - Default values from environment, defined by your field names ([eg-env](https://github.com/jpillora/opts-examples/tree/master/eg-env/)) 64 | - Repeated flags using slices ([eg-repeated-flag](https://github.com/jpillora/opts-examples/tree/master/eg-repeated-flag/)) 65 | - Group your flags in the help output ([eg-groups](https://github.com/jpillora/opts-examples/tree/master/eg-groups/)) 66 | - Sub-commands by nesting structs ([eg-commands-inline](https://github.com/jpillora/opts-examples/tree/master/eg-commands-inline/)) 67 | - Sub-commands by providing child `Opts` ([eg-commands-main](https://github.com/jpillora/opts-examples/tree/master/eg-commands-main/)) 68 | - Infers program name from executable name 69 | - Infers command names from struct or package name 70 | - Define custom flags types via `opts.Setter` or `flag.Value` ([eg-custom-flag](https://github.com/jpillora/opts-examples/tree/master/eg-custom-flag/)) 71 | - Customizable help text by modifying the default templates ([eg-help](https://github.com/jpillora/opts-examples/tree/master/eg-help/)) 72 | - Built-in shell auto-completion ([eg-complete](https://github.com/jpillora/opts-examples/tree/master/eg-complete)) 73 | 74 | Find these examples and more in the [`opts-examples`](https://github.com/jpillora/opts-examples) repository. 75 | 76 | ### Package API 77 | 78 | See https://godoc.org/github.com/jpillora/opts#Opts 79 | 80 | [![GoDoc](https://godoc.org/github.com/jpillora/opts?status.svg)](https://godoc.org/github.com/jpillora/opts) 81 | 82 | ### Struct Tag API 83 | 84 | **opts** tries to set sane defaults so, for the most part, you'll get the desired behaviour by simply providing a configuration struct. 85 | 86 | However, you can customise this behaviour by providing the `opts` struct 87 | tag with a series of settings in the form of **`key=value`**: 88 | 89 | ``` 90 | `opts:"key=value,key=value,..."` 91 | ``` 92 | 93 | Where **`key`** must be one of: 94 | 95 | - `-` (dash) - Like `json:"-"`, the dash character will cause opts to ignore the struct field. Unexported fields are always ignored. 96 | 97 | - `name` - Name is used to display the field in the help text. By default, the flag name is infered by converting the struct field name to lowercase and adding dashes between words. 98 | 99 | - `help` - The help text used to summaryribe the field. It will be displayed next to the flag name in the help output. 100 | 101 | *Note:* `help` can also be set as a stand-alone struct tag (i.e. `help:"my text goes here"`). You must use the stand-alone tag if you wish to use a comma `,` in your help string. 102 | 103 | - `mode` - The **opts** mode assigned to the field. All fields will be given a `mode`. Where the `mode` **`value`** must be one of: 104 | 105 | * `flag` - The field will be treated as a flag: an optional, named, configurable field. Set using `./program -- `. The struct field must be a [*flag-value*](#flag-values) type. `flag` is the default `mode` for any [*flag-value*](#flag-values). 106 | 107 | * `arg` - The field will be treated as an argument: a required, positional, unamed, configurable field. Set using `./program `. The struct field must be a [*flag-value*](#flag-values) type. 108 | 109 | * `embedded` - A special mode which causes the fields of struct to be used in the current struct. Useful if you want to split your command-line options across multiple files (default for `struct` fields). The struct field must be a `struct`. `embedded` is the default `mode` for a `struct`. *Tip* You can play group all fields together placing an `group` tag on the struct field. 110 | 111 | * `cmd` - A inline command, shorthand for `.AddCommmand(opts.New(&field))`, which also implies the struct field must be a `struct`. 112 | 113 | * `cmdname` - A special mode which will assume the name of the selected command. The struct field must be a `string`. 114 | 115 | - `short` - One letter to be used a flag's "short" name. By default, the first letter of `name` will be used. It will remain unset if there is a duplicate short name or if `opts:"short=-"`. Only valid when `mode` is `flag`. 116 | 117 | - `group` - The name of the flag group to store the field. Defining this field will create a new group of flags in the help text (will appear as "`` options"). The default flag group is the empty string (which will appear as "Options"). Only valid when `mode` is `flag` or `embedded`. 118 | 119 | - `env` - An environent variable to use as the field's **default** value. It can always be overridden by providing the appropriate flag. Only valid when `mode` is `flag`. 120 | 121 | For example, `opts:"env=FOO"`. It can also be infered using the field name with simply `opts:"env"`. You can enable inference on all flags with the `opts.Opts` method `UseEnv()`. 122 | 123 | - `min` `max` - A minimum or maximum length of a slice. Only valid when `mode` is `arg`, *and* the struct field is a slice. 124 | 125 | #### flag-values: 126 | 127 | In general an opts _flag-value_ type aims to be any type that can be get and set using a `string`. Currently, **opts** supports the following types: 128 | 129 | - `string` 130 | - `bool` 131 | - `int`, `int8`, `int16`, `int32`, `int64` 132 | - `uint`, `uint8`, `uint16`, `uint32`, `uint64` 133 | - `float32`, `float64` 134 | - [`opts.Setter`](https://godoc.org/github.com/jpillora/opts#Setter) 135 | - *The interface `func Set(string) error`* 136 | - [`flag.Value`](https://golang.org/pkg/flag/#Value) 137 | - *Is an `opts.Setter`* 138 | - `time.Duration` 139 | - `encoding.TextUnmarshaler` 140 | - *Includes `time.Time` and `net.IP`* 141 | - `encoding.BinaryUnmarshaler` 142 | - *Includes `url.URL`* 143 | 144 | In addition, `flag`s and `arg`s can also be a slice of any _flag-value_ type. Slices allow multiple flags/args. For example, a struct field flag `Foo []int` could be set with `--foo 1 --foo 2`, and would result in `[]int{1,2}`. 145 | 146 | ### Help text 147 | 148 | By default, **opts** attempts to output well-formatted help text when the user provides the `--help` (`-h`) flag. The [examples](https://github.com/jpillora/opts-examples) repositories shows various combinations of this default help text, resulting from using various features above. 149 | 150 | Modifications be made by customising the underlying [Go templates](https://golang.org/pkg/text/template/) found here [DefaultTemplates](https://godoc.org/github.com/jpillora/opts#pkg-variables). 151 | 152 | ### Talk 153 | 154 | I gave a talk on **opts** at the Go Meetup Sydney (golang-syd) on the 23rd of May, 2019. You can find the slides here https://github.com/jpillora/opts-talk. 155 | 156 | ### Other projects 157 | 158 | Other related projects which infer flags from struct tags but aren't as feature-complete: 159 | 160 | - https://github.com/alexflint/go-arg 161 | - https://github.com/jessevdk/go-flags 162 | 163 | #### MIT License 164 | 165 | Copyright © 2019 <dev@jpillora.com> 166 | 167 | Permission is hereby granted, free of charge, to any person obtaining 168 | a copy of this software and associated documentation files (the 169 | 'Software'), to deal in the Software without restriction, including 170 | without limitation the rights to use, copy, modify, merge, publish, 171 | distribute, sublicense, and/or sell copies of the Software, and to 172 | permit persons to whom the Software is furnished to do so, subject to 173 | the following conditions: 174 | 175 | The above copyright notice and this permission notice shall be 176 | included in all copies or substantial portions of the Software. 177 | 178 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 179 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 180 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 181 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 182 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 183 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 184 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 185 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | //Package opts defines a struct-tag based API for 2 | //rapidly building command-line interfaces. For example: 3 | // 4 | // package main 5 | // 6 | // import ( 7 | // "log" 8 | // "github.com/jpillora/opts" 9 | // ) 10 | // 11 | // func main() { 12 | // type config struct { 13 | // File string `opts:"help=file to load"` 14 | // Lines int `opts:"help=number of lines to show"` 15 | // } 16 | // c := config{} 17 | // opts.Parse(&c) 18 | // log.Printf("%+v", c) 19 | // } 20 | // 21 | //Build and run: 22 | // 23 | // $ go build -o my-prog 24 | // $ ./my-prog --help 25 | // 26 | // Usage: my-prog [options] 27 | // 28 | // Options: 29 | // --file, -f file to load 30 | // --lines, -l number of lines to show 31 | // --help, -h display help 32 | // 33 | // $ ./my-prog -f foo.txt -l 42 34 | // {File:foo.txt Lines:42} 35 | // 36 | //See https://github.com/jpillora/opts for more information and more examples. 37 | package opts 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jpillora/opts 2 | 3 | go 1.12 4 | 5 | require github.com/posener/complete v1.2.2-0.20190308074557-af07aa5181b3 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 2 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 3 | github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= 4 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 5 | github.com/posener/complete v1.2.2-0.20190308074557-af07aa5181b3 h1:GqpA1/5oN1NgsxoSA4RH0YWTaqvUlQNeOpHXD/JRbOQ= 6 | github.com/posener/complete v1.2.2-0.20190308074557-af07aa5181b3/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= 7 | -------------------------------------------------------------------------------- /item.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "encoding" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | "time" 9 | ) 10 | 11 | //item group represents a single "Options" block 12 | //in the help text ouput 13 | type itemGroup struct { 14 | name string 15 | flags []*item 16 | } 17 | 18 | const defaultGroup = "" 19 | 20 | //item is the structure representing a 21 | //an opt item. it also implements flag.Value 22 | //generically using reflect. 23 | type item struct { 24 | val reflect.Value 25 | mode string 26 | name string 27 | shortName string 28 | envName string 29 | useEnv bool 30 | help string 31 | defstr string 32 | slice bool 33 | min, max int //valid if slice 34 | noarg bool 35 | completer Completer 36 | sets int 37 | } 38 | 39 | func newItem(val reflect.Value) (*item, error) { 40 | if !val.IsValid() { 41 | return nil, fmt.Errorf("invalid value") 42 | } 43 | i := &item{} 44 | supported := false 45 | //take interface value V 46 | v := val.Interface() 47 | pv := interface{}(nil) 48 | if val.CanAddr() { 49 | pv = val.Addr().Interface() 50 | } 51 | //convert V or &V into a setter: 52 | for _, t := range []interface{}{v, pv} { 53 | if tm, ok := t.(encoding.TextUnmarshaler); ok { 54 | v = &textValue{tm} 55 | } else if bm, ok := t.(encoding.BinaryUnmarshaler); ok { 56 | v = &binaryValue{bm} 57 | } else if d, ok := t.(*time.Duration); ok { 58 | v = newDurationValue(d) 59 | } else if s, ok := t.(Setter); ok { 60 | v = s 61 | } 62 | } 63 | //implements setter (flag.Value)? 64 | if s, ok := v.(Setter); ok { 65 | supported = true 66 | //NOTE: replacing val removes our ability to set 67 | //the value, resolved by flag.Value handling all Set calls. 68 | val = reflect.ValueOf(s) 69 | } 70 | //implements completer? 71 | if c, ok := v.(Completer); ok { 72 | i.completer = c 73 | } 74 | //val must be concrete at this point 75 | if val.Kind() == reflect.Ptr { 76 | val = val.Elem() 77 | } 78 | //lock in val 79 | i.val = val 80 | i.slice = val.Kind() == reflect.Slice 81 | //prevent defaults on slices (should vals be appended? should it be reset? how to display defaults?) 82 | if i.slice && val.Len() > 0 { 83 | return nil, fmt.Errorf("slices cannot have default values") 84 | } 85 | //type checks 86 | t := i.elemType() 87 | if t.Kind() == reflect.Ptr { 88 | return nil, fmt.Errorf("slice elem (%s) cannot be a pointer", t.Kind()) 89 | } else if i.slice && t.Kind() == reflect.Bool { 90 | return nil, fmt.Errorf("slice of bools not supported") 91 | } 92 | switch t.Kind() { 93 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 94 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 95 | reflect.Float32, reflect.Float64, 96 | reflect.String, reflect.Bool: 97 | supported = true 98 | } 99 | //use the inner bool flag, if defined, otherwise if bool 100 | if bf, ok := v.(interface{ IsBoolFlag() bool }); ok { 101 | i.noarg = bf.IsBoolFlag() 102 | } else if t.Kind() == reflect.Bool { 103 | i.noarg = true 104 | } 105 | if !supported { 106 | return nil, fmt.Errorf("field type not supported: %s", t.Kind()) 107 | } 108 | return i, nil 109 | } 110 | 111 | func (i *item) set() bool { 112 | return i.sets != 0 113 | } 114 | 115 | func (i *item) elemType() reflect.Type { 116 | t := i.val.Type() 117 | if i.slice { 118 | t = t.Elem() 119 | } 120 | return t 121 | } 122 | 123 | func (i *item) String() string { 124 | if !i.val.IsValid() { 125 | return "" 126 | } 127 | v := i.val.Interface() 128 | if s, ok := v.(fmt.Stringer); ok { 129 | return s.String() 130 | } 131 | return fmt.Sprintf("%v", v) 132 | } 133 | 134 | func (i *item) Set(s string) error { 135 | //can only set singles once 136 | if i.sets != 0 && !i.slice { 137 | return errors.New("already set") 138 | } 139 | //set has two modes, slice and inplace. 140 | // when slice, create a new zero value, scan into it, append to slice 141 | // when inplace, take pointer, scan into it 142 | var elem reflect.Value 143 | if i.slice { 144 | elem = reflect.New(i.elemType()) //ptr 145 | } else if i.val.CanAddr() { 146 | elem = i.val.Addr() //pointer to concrete type 147 | } else { 148 | elem = i.val //possibly interface type 149 | } 150 | v := elem.Interface() 151 | //convert string into value 152 | if fv, ok := v.(Setter); ok { 153 | //addr implements set 154 | if err := fv.Set(s); err != nil { 155 | return err 156 | } 157 | } else if elem.Kind() == reflect.Ptr && elem.Elem().Kind() == reflect.String { 158 | src := reflect.ValueOf(s) 159 | dst := elem.Elem() 160 | //convert custom string types 161 | st := src.Type() 162 | dt := dst.Type() 163 | if !st.AssignableTo(dt) { 164 | //should always be convertable since kind==string 165 | if !st.ConvertibleTo(dt) { 166 | return fmt.Errorf("cannot convert %s to %s", st, dt) 167 | } 168 | src = src.Convert(dt) 169 | } 170 | dst.Set(src) 171 | } else if elem.Kind() == reflect.Ptr { 172 | //magic set with scanf 173 | n, err := fmt.Sscanf(s, "%v", v) 174 | if err != nil { 175 | return err 176 | } else if n == 0 { 177 | return errors.New("could not be parsed") 178 | } 179 | } else { 180 | return errors.New("could not be set") 181 | } 182 | //slice? append! 183 | if i.slice { 184 | //no pointer elems 185 | if elem.Kind() == reflect.Ptr { 186 | elem = elem.Elem() 187 | } 188 | //append! 189 | i.val.Set(reflect.Append(i.val, elem)) 190 | } 191 | //mark item as set! 192 | i.sets++ 193 | //done 194 | return nil 195 | } 196 | 197 | //IsBoolFlag implements the hidden interface 198 | //documented here https://golang.org/pkg/flag/#Value 199 | func (i *item) IsBoolFlag() bool { 200 | return i.noarg 201 | } 202 | 203 | //noopValue defines a flag value which does nothing 204 | var noopValue = noopValueType(0) 205 | 206 | type noopValueType int 207 | 208 | func (noopValueType) String() string { 209 | return "" 210 | } 211 | 212 | func (noopValueType) Set(s string) error { 213 | return nil 214 | } 215 | 216 | //textValue wraps marshaller into a setter 217 | type textValue struct { 218 | encoding.TextUnmarshaler 219 | } 220 | 221 | func (t textValue) Set(s string) error { 222 | return t.UnmarshalText([]byte(s)) 223 | } 224 | 225 | //binaryValue wraps marshaller into a setter 226 | type binaryValue struct { 227 | encoding.BinaryUnmarshaler 228 | } 229 | 230 | func (t binaryValue) Set(s string) error { 231 | return t.UnmarshalBinary([]byte(s)) 232 | } 233 | 234 | //borrowed from the stdlib :) 235 | type durationValue time.Duration 236 | 237 | func newDurationValue(p *time.Duration) *durationValue { 238 | return (*durationValue)(p) 239 | } 240 | 241 | func (d *durationValue) Set(s string) error { 242 | v, err := time.ParseDuration(s) 243 | if err != nil { 244 | return err 245 | } 246 | *d = durationValue(v) 247 | return nil 248 | } 249 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "flag" 5 | "reflect" 6 | ) 7 | 8 | // node is the main class, it contains 9 | // all parsing state for a single set of 10 | // arguments 11 | type node struct { 12 | err error 13 | //embed item since an node can also be an item 14 | item 15 | parent *node 16 | flagGroups []*itemGroup 17 | flagNames map[string]bool //flag namespace covers all groups in this node 18 | flagSkipShort map[string]bool 19 | args []*item 20 | envNames map[string]bool 21 | userCfgPath bool 22 | //external flagsets 23 | flagsets []*flag.FlagSet 24 | //subcommands 25 | cmd *node 26 | cmdname *string 27 | cmdnameEnv string 28 | cmds map[string]*node 29 | //help 30 | order []string 31 | templates map[string]string 32 | repo, author, version, summary string 33 | repoInfer, authorInfer bool 34 | lineWidth int 35 | padAll bool 36 | padWidth int 37 | //pretend these are in the user struct :) 38 | internalOpts struct { 39 | Help bool 40 | Version bool 41 | Install bool 42 | Uninstall bool 43 | ConfigPath string 44 | } 45 | complete bool 46 | } 47 | 48 | func newNode(val reflect.Value) *node { 49 | n := &node{ 50 | parent: nil, 51 | //each cmd/cmd has its own set of names 52 | flagNames: map[string]bool{}, 53 | flagSkipShort: map[string]bool{}, 54 | envNames: map[string]bool{}, 55 | cmds: map[string]*node{}, 56 | //these are only set at the root 57 | order: defaultOrder(), 58 | templates: map[string]string{}, 59 | //public defaults 60 | lineWidth: 96, 61 | padAll: true, 62 | padWidth: 2, 63 | } 64 | //all new node's MUST be an addressable struct 65 | t := val.Type() 66 | if t.Kind() == reflect.Ptr { 67 | t = t.Elem() 68 | val = val.Elem() 69 | } 70 | if !val.CanAddr() || t.Kind() != reflect.Struct { 71 | n.errorf("must be an addressable to a struct") 72 | return n 73 | } 74 | n.item.val = val 75 | return n 76 | } 77 | -------------------------------------------------------------------------------- /node_build.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | //errorf to be stored until parse-time 9 | func (n *node) errorf(format string, args ...interface{}) error { 10 | err := authorError(fmt.Sprintf(format, args...)) 11 | //only store the first error 12 | if n.err == nil { 13 | n.err = err 14 | } 15 | return err 16 | } 17 | 18 | func (n *node) Name(name string) Opts { 19 | n.name = name 20 | return n 21 | } 22 | 23 | func (n *node) Version(version string) Opts { 24 | n.version = version 25 | return n 26 | } 27 | 28 | func (n *node) Summary(summary string) Opts { 29 | n.summary = summary 30 | return n 31 | } 32 | 33 | func (n *node) Repo(repo string) Opts { 34 | n.repo = repo 35 | return n 36 | } 37 | 38 | func (n *node) PkgRepo() Opts { 39 | n.repoInfer = true 40 | return n 41 | } 42 | 43 | func (n *node) Author(author string) Opts { 44 | n.author = author 45 | return n 46 | } 47 | 48 | //PkgRepo infers the repository link of the program 49 | //from the package import path of the struct (So note, 50 | //this will not work for 'main' packages) 51 | func (n *node) PkgAuthor() Opts { 52 | n.authorInfer = true 53 | return n 54 | } 55 | 56 | //Set the padding width, which defines the amount padding 57 | //when rendering help text (defaults to 72) 58 | func (n *node) SetPadWidth(p int) Opts { 59 | n.padWidth = p 60 | return n 61 | } 62 | 63 | func (n *node) DisablePadAll() Opts { 64 | n.padAll = false 65 | return n 66 | } 67 | 68 | func (n *node) SetLineWidth(l int) Opts { 69 | n.lineWidth = l 70 | return n 71 | } 72 | 73 | func (n *node) ConfigPath(path string) Opts { 74 | n.internalOpts.ConfigPath = path 75 | return n 76 | } 77 | 78 | func (n *node) UserConfigPath() Opts { 79 | n.userCfgPath = true 80 | return n 81 | } 82 | 83 | func (n *node) UseEnv() Opts { 84 | n.useEnv = true 85 | return n 86 | } 87 | 88 | //DocBefore inserts a text block before the specified template 89 | func (n *node) DocBefore(target, newID, template string) Opts { 90 | return n.docOffset(0, target, newID, template) 91 | } 92 | 93 | //DocAfter inserts a text block after the specified template 94 | func (n *node) DocAfter(target, newID, template string) Opts { 95 | return n.docOffset(1, target, newID, template) 96 | } 97 | 98 | //DecSet replaces the specified template 99 | func (n *node) DocSet(id, template string) Opts { 100 | if _, ok := DefaultTemplates[id]; !ok { 101 | if _, ok := n.templates[id]; !ok { 102 | n.errorf("template does not exist: %s", id) 103 | return n 104 | } 105 | } 106 | n.templates[id] = template 107 | return n 108 | } 109 | 110 | func (n *node) docOffset(offset int, target, newID, template string) *node { 111 | if _, ok := n.templates[newID]; ok { 112 | n.errorf("new template already exists: %s", newID) 113 | return n 114 | } 115 | for i, id := range n.order { 116 | if id == target { 117 | n.templates[newID] = template 118 | index := i + offset 119 | rest := []string{newID} 120 | if index < len(n.order) { 121 | rest = append(rest, n.order[index:]...) 122 | } 123 | n.order = append(n.order[:index], rest...) 124 | return n 125 | } 126 | } 127 | n.errorf("target template not found: %s", target) 128 | return n 129 | } 130 | 131 | func (n *node) EmbedFlagSet(fs *flag.FlagSet) Opts { 132 | n.flagsets = append(n.flagsets, fs) 133 | return n 134 | } 135 | 136 | func (n *node) EmbedGlobalFlagSet() Opts { 137 | return n.EmbedFlagSet(flag.CommandLine) 138 | } 139 | 140 | func (n *node) Call(fn func(o Opts)) Opts { 141 | fn(n) 142 | return n 143 | } 144 | 145 | func (n *node) flagGroup(name string) *itemGroup { 146 | //NOTE: the default group is the empty string 147 | //get existing group 148 | for _, g := range n.flagGroups { 149 | if g.name == name { 150 | return g 151 | } 152 | } 153 | //otherwise, create and append 154 | g := &itemGroup{name: name} 155 | n.flagGroups = append(n.flagGroups, g) 156 | return g 157 | } 158 | 159 | func (n *node) flags() []*item { 160 | flags := []*item{} 161 | for _, g := range n.flagGroups { 162 | flags = append(flags, g.flags...) 163 | } 164 | return flags 165 | } 166 | 167 | type authorError string 168 | 169 | func (e authorError) Error() string { 170 | return string(e) 171 | } 172 | 173 | type exitError string 174 | 175 | func (e exitError) Error() string { 176 | return string(e) 177 | } 178 | -------------------------------------------------------------------------------- /node_commands.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | ) 8 | 9 | func (n *node) AddCommand(cmd Opts) Opts { 10 | sub, ok := cmd.(*node) 11 | if !ok { 12 | panic("another implementation of opts???") 13 | } 14 | //default name should be package name, 15 | //unless its in the main package, then 16 | //the default becomes the struct name 17 | structType := sub.item.val.Type() 18 | pkgPath := structType.PkgPath() 19 | if sub.name == "" && pkgPath != "main" && pkgPath != "" { 20 | _, sub.name = path.Split(pkgPath) 21 | } 22 | structName := structType.Name() 23 | if sub.name == "" && structName != "" { 24 | sub.name = camel2dash(structName) 25 | } 26 | //if still no name, needs to be manually set 27 | if sub.name == "" { 28 | n.errorf("cannot add command, please set a Name()") 29 | return n 30 | } 31 | if _, exists := n.cmds[sub.name]; exists { 32 | n.errorf("cannot add command, '%s' already exists", sub.name) 33 | return n 34 | } 35 | sub.parent = n 36 | n.cmds[sub.name] = sub 37 | return n 38 | } 39 | 40 | func (n *node) matchedCommand() *node { 41 | if n.cmd != nil { 42 | return n.cmd.matchedCommand() 43 | } 44 | return n 45 | } 46 | 47 | //IsRunnable 48 | func (n *node) IsRunnable() bool { 49 | _, ok, _ := n.run(true) 50 | return ok 51 | } 52 | 53 | //Run the parsed configuration 54 | func (n *node) Run() error { 55 | _, _, err := n.run(false) 56 | return err 57 | } 58 | 59 | //Selected returns the subcommand picked when parsing the command line 60 | func (n *node) Selected() ParsedOpts { 61 | m := n.matchedCommand() 62 | return m 63 | } 64 | 65 | type runner1 interface { 66 | Run() error 67 | } 68 | 69 | type runner2 interface { 70 | Run() 71 | } 72 | 73 | func (n *node) run(test bool) (ParsedOpts, bool, error) { 74 | m := n.matchedCommand() 75 | v := m.val.Addr().Interface() 76 | r1, ok1 := v.(runner1) 77 | r2, ok2 := v.(runner2) 78 | if test { 79 | return m, ok1 || ok2, nil 80 | } 81 | if ok1 { 82 | return m, true, r1.Run() 83 | } 84 | if ok2 { 85 | r2.Run() 86 | return m, true, nil 87 | } 88 | if len(m.cmds) > 0 { 89 | //if matched command has no run, 90 | //but has commands, show help instead 91 | return m, false, fmt.Errorf("sub command '%s' is not runnable", m.name) 92 | } 93 | return m, false, fmt.Errorf("command '%s' is not runnable", m.name) 94 | } 95 | 96 | //Run the parsed configuration 97 | func (n *node) RunFatal() { 98 | if err := n.Run(); err != nil { 99 | fmt.Fprint(os.Stderr, err.Error()) 100 | os.Exit(1) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /node_complete.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/posener/complete" 10 | "github.com/posener/complete/cmd/install" 11 | ) 12 | 13 | //NOTE: Currently completion internally uses posener/complete. 14 | //in future this may change to another implementation. 15 | 16 | //Completer represents a shell-completion implementation 17 | //for a single field. By default, all fields will auto-complete 18 | //files and directories. Use a field type which implements this 19 | //Completer interface to override this behaviour. 20 | type Completer interface { 21 | //Complete is given user's input and should 22 | //return a corresponding set of valid inputs. 23 | //Note: all result strings must be prefixed 24 | //with the user's input. 25 | Complete(user string) []string 26 | } 27 | 28 | //Complete enables shell-completion for this command and 29 | //its subcommands 30 | func (n *node) Complete() Opts { 31 | n.complete = true 32 | return n 33 | } 34 | 35 | func (n *node) manageCompletion(uninstall bool) error { 36 | msg := "" 37 | fn := install.Install 38 | if uninstall { 39 | fn = install.Uninstall 40 | } 41 | if err := fn(n.name); err != nil { 42 | w, ok := err.(interface{ WrappedErrors() []error }) 43 | if ok { 44 | for _, w := range w.WrappedErrors() { 45 | msg += strings.TrimPrefix(fmt.Sprintf("%s\n", w), "does ") 46 | } 47 | } else { 48 | msg = fmt.Sprintf("%v\n", err) 49 | } 50 | } else if uninstall { 51 | msg = "Uninstalled\n" 52 | } else { 53 | msg = "Installed\n" 54 | } 55 | return exitError(msg) //always exit 56 | } 57 | 58 | func (n *node) doCompletion() bool { 59 | return complete.New(n.name, n.nodeCompletion()).Complete() 60 | } 61 | 62 | func (n *node) nodeCompletion() complete.Command { 63 | //make a completion command for this node 64 | c := complete.Command{ 65 | Sub: complete.Commands{}, 66 | Flags: complete.Flags{}, 67 | GlobalFlags: nil, 68 | Args: nil, 69 | } 70 | //prepare flags 71 | for _, item := range n.flags() { 72 | //item's predictor 73 | var p complete.Predictor 74 | //choose a predictor 75 | if item.noarg { 76 | //disable 77 | p = complete.PredictNothing 78 | } else if item.completer != nil { 79 | //user completer 80 | p = &completerWrapper{ 81 | compl: item.completer, 82 | } 83 | } else { 84 | //by default, predicts files and directories 85 | p = &completerWrapper{ 86 | compl: &completerFS{}, 87 | } 88 | } 89 | //add to completion flags set 90 | c.Flags["--"+item.name] = p 91 | if item.shortName != "" { 92 | c.Flags["-"+item.shortName] = p 93 | } 94 | } 95 | //prepare args 96 | if len(n.args) > 0 { 97 | c.Args = &completerWrapper{ 98 | compl: &completerFS{}, 99 | } 100 | } 101 | //prepare sub-commands 102 | for name, subn := range n.cmds { 103 | c.Sub[name] = subn.nodeCompletion() //recurse 104 | } 105 | return c 106 | } 107 | 108 | type completerWrapper struct { 109 | compl Completer 110 | } 111 | 112 | func (w *completerWrapper) Predict(args complete.Args) []string { 113 | user := args.Last 114 | results := w.compl.Complete(user) 115 | if os.Getenv("OPTS_DEBUG") == "1" { 116 | debugf("'%s' => %v", user, results) 117 | } 118 | return results 119 | } 120 | 121 | type completerFS struct{} 122 | 123 | func (*completerFS) Complete(user string) []string { 124 | home := os.Getenv("HOME") 125 | if home != "" && strings.HasPrefix(user, "~/") { 126 | user = home + "/" + strings.TrimPrefix(user, "~/") 127 | } 128 | completed := []string{} 129 | matches, _ := filepath.Glob(user + "*") 130 | for _, m := range matches { 131 | if home != "" && strings.HasPrefix(m, home) { 132 | m = "~" + strings.TrimPrefix(m, home) 133 | } 134 | if !strings.HasPrefix(m, user) { 135 | continue 136 | } 137 | completed = append(completed, m) 138 | } 139 | return matches 140 | } 141 | 142 | func debugf(f string, a ...interface{}) { 143 | l, err := os.OpenFile("/tmp/opts.debug", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) 144 | if err == nil { 145 | fmt.Fprintf(l, f+"\n", a...) 146 | l.Close() 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /node_help.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "regexp" 9 | "sort" 10 | "strings" 11 | "text/template" 12 | ) 13 | 14 | // data is only used for templating below 15 | type data struct { 16 | datum //data is also a datum 17 | FlagGroups []*datumGroup 18 | Args []*datum 19 | Cmds []*datum 20 | Order []string 21 | Parents string 22 | Version string 23 | Summary string 24 | Repo, Author string 25 | ErrMsg string 26 | } 27 | 28 | type datum struct { 29 | Name, Help, Pad string //Pad is Opt.padWidth many spaces 30 | } 31 | 32 | type datumGroup struct { 33 | Name string 34 | Flags []*datum 35 | } 36 | 37 | // DefaultOrder defines which templates get rendered in which order. 38 | // This list is referenced in the "help" template below. 39 | var DefaultOrder = []string{ 40 | "usage", 41 | "summary", 42 | "args", 43 | "flaggroups", 44 | "cmds", 45 | "author", 46 | "version", 47 | "repo", 48 | "errmsg", 49 | } 50 | 51 | func defaultOrder() []string { 52 | order := make([]string, len(DefaultOrder)) 53 | copy(order, DefaultOrder) 54 | return order 55 | } 56 | 57 | // DefaultTemplates define a set of individual templates 58 | // that get rendered in DefaultOrder. You can replace templates or insert templates before or after existing 59 | // templates using the DocSet, DocBefore and DocAfter methods. For example, you can insert a string after the 60 | // usage text with: 61 | // 62 | // DocAfter("usage", "this is a string, and if it is very long, it will be wrapped") 63 | // 64 | // The entire help text is simply the "help" template listed below, which renders a set of these templates in 65 | // the order defined above. All templates can be referenced using the keys in this map: 66 | var DefaultTemplates = map[string]string{ 67 | "help": `{{ $root := . }}{{range $t := .Order}}{{ templ $t $root }}{{end}}`, 68 | "usage": `Usage: {{.Name }} [options]{{template "usageargs" .}}{{template "usagecmd" .}}` + "\n", 69 | "usageargs": `{{range .Args}} {{.Name}}{{end}}`, 70 | "usagecmd": `{{if .Cmds}} {{end}}`, 71 | "extradefault": `{{if .}}default {{.}}{{end}}`, 72 | "extraenv": `{{if .}}env {{.}}{{end}}`, 73 | "extramultiple": `{{if .}}allows multiple{{end}}`, 74 | "summary": "{{if .Summary}}\n{{ .Summary }}\n{{end}}", 75 | "args": `{{range .Args}}{{template "arg" .}}{{end}}`, 76 | "arg": "{{if .Help}}\n{{.Help}}\n{{end}}", 77 | "flaggroups": `{{ range $g := .FlagGroups}}{{template "flaggroup" $g}}{{end}}`, 78 | "flaggroup": "{{if .Flags}}\n{{if .Name}}{{.Name}} options{{else}}Options{{end}}:\n" + 79 | `{{ range $f := .Flags}}{{template "flag" $f}}{{end}}{{end}}`, 80 | "flag": `{{.Name}}{{if .Help}}{{.Pad}}{{.Help}}{{end}}` + "\n", 81 | "cmds": "{{if .Cmds}}\nCommands:\n" + `{{ range $sub := .Cmds}}{{template "cmd" $sub}}{{end}}{{end}}`, 82 | "cmd": "· {{ .Name }}{{if .Help}}{{.Pad}} {{ .Help }}{{end}}\n", 83 | "version": "{{if .Version}}\nVersion:\n{{.Pad}}{{.Version}}\n{{end}}", 84 | "repo": "{{if .Repo}}\nRead more:\n{{.Pad}}{{.Repo}}\n{{end}}", 85 | "author": "{{if .Author}}\nAuthor:\n{{.Pad}}{{.Author}}\n{{end}}", 86 | "errmsg": "{{if .ErrMsg}}\nError:\n{{.Pad}}{{.ErrMsg}}\n{{end}}", 87 | } 88 | 89 | var ( 90 | trailingSpaces = regexp.MustCompile(`(?m)\ +$`) 91 | trailingBrackets = regexp.MustCompile(`^(.+)\(([^\)]+)\)$`) 92 | ) 93 | 94 | // Help renders the help text as a string 95 | func (o *node) Help() string { 96 | h, err := renderHelp(o) 97 | if err != nil { 98 | log.Fatalf("render help failed: %s", err) 99 | } 100 | return h 101 | } 102 | 103 | func renderHelp(o *node) (string, error) { 104 | var err error 105 | //add default templates 106 | for name, str := range DefaultTemplates { 107 | if _, ok := o.templates[name]; !ok { 108 | o.templates[name] = str 109 | } 110 | } 111 | //prepare templates 112 | t := template.New(o.name) 113 | t = t.Funcs(map[string]interface{}{ 114 | //reimplementation of "template" except with dynamic name 115 | "templ": func(name string, data interface{}) (string, error) { 116 | b := &bytes.Buffer{} 117 | err = t.ExecuteTemplate(b, name, data) 118 | if err != nil { 119 | return "", err 120 | } 121 | return b.String(), nil 122 | }, 123 | }) 124 | //parse all templates and "define" themselves as nested templates 125 | for name, str := range o.templates { 126 | t, err = t.Parse(fmt.Sprintf(`{{define "%s"}}%s{{end}}`, name, str)) 127 | if err != nil { 128 | return "", fmt.Errorf("template '%s': %s", name, err) 129 | } 130 | } 131 | //convert node into template data 132 | tf, err := convert(o) 133 | if err != nil { 134 | return "", fmt.Errorf("node convert: %s", err) 135 | } 136 | //execute all templates 137 | b := &bytes.Buffer{} 138 | err = t.ExecuteTemplate(b, "help", tf) 139 | if err != nil { 140 | return "", fmt.Errorf("template execute: %s", err) 141 | } 142 | out := b.String() 143 | if o.padAll { 144 | /* 145 | "foo 146 | bar" 147 | becomes 148 | " 149 | foo 150 | bar 151 | " 152 | */ 153 | lines := strings.Split(out, "\n") 154 | for i, l := range lines { 155 | lines[i] = tf.Pad + l 156 | } 157 | out = "\n" + strings.Join(lines, "\n") + "\n" 158 | } 159 | out = trailingSpaces.ReplaceAllString(out, "") 160 | return out, nil 161 | } 162 | 163 | func convert(o *node) (*data, error) { 164 | names := []string{} 165 | curr := o 166 | for curr != nil { 167 | names = append([]string{curr.name}, names...) 168 | curr = curr.parent 169 | } 170 | name := strings.Join(names, " ") 171 | args := make([]*datum, len(o.args)) 172 | for i, arg := range o.args { 173 | //arguments are required 174 | n := "<" + arg.name + ">" 175 | //unless... 176 | if arg.slice { 177 | p := []string{arg.name, arg.name} 178 | for i, n := range p { 179 | if i < arg.min { 180 | //still required 181 | n = "<" + n + ">" 182 | } else { 183 | //optional! 184 | n = "[" + n + "]" 185 | } 186 | p[i] = n 187 | } 188 | n = strings.Join(p, " ") + " ..." 189 | } 190 | args[i] = &datum{ 191 | Name: n, 192 | Help: constrain(arg.help, o.lineWidth), 193 | } 194 | } 195 | flagGroups := make([]*datumGroup, len(o.flagGroups)) 196 | //initialise and calculate padding 197 | max := 0 198 | pad := nletters(' ', o.padWidth) 199 | for i, g := range o.flagGroups { 200 | dg := &datumGroup{ 201 | Name: g.name, 202 | Flags: make([]*datum, len(g.flags)), 203 | } 204 | flagGroups[i] = dg 205 | for i, item := range g.flags { 206 | to := &datum{Pad: pad} 207 | to.Name = "--" + item.name 208 | if item.shortName != "" && !o.flagSkipShort[item.name] { 209 | to.Name += ", -" + item.shortName 210 | } 211 | l := len(to.Name) 212 | //max shared across ALL groups 213 | if l > max { 214 | max = l 215 | } 216 | dg.Flags[i] = to 217 | } 218 | } 219 | //get item help, with optional default values and env names and 220 | //constrain to a specific line width 221 | extras := make([]*template.Template, 3) 222 | keys := []string{"default", "env", "multiple"} 223 | for i, k := range keys { 224 | t, err := template.New("").Parse(o.templates["extra"+k]) 225 | if err != nil { 226 | return nil, fmt.Errorf("template extra%s: %s", k, err) 227 | } 228 | extras[i] = t 229 | } 230 | //calculate... 231 | padsInOption := o.padWidth 232 | optionNameWidth := max + padsInOption 233 | spaces := nletters(' ', optionNameWidth) 234 | helpWidth := o.lineWidth - optionNameWidth 235 | //go back and render each option using calculated values 236 | for i, dg := range flagGroups { 237 | for j, to := range dg.Flags { 238 | //pad all option names to be the same length 239 | to.Name += spaces[:max-len(to.Name)] 240 | //constrain help text 241 | item := o.flagGroups[i].flags[j] 242 | //render flag help string 243 | vals := []interface{}{item.defstr, item.envName, item.slice} 244 | outs := []string{} 245 | for i, v := range vals { 246 | b := strings.Builder{} 247 | if err := extras[i].Execute(&b, v); err != nil { 248 | return nil, err 249 | } 250 | if b.Len() > 0 { 251 | outs = append(outs, b.String()) 252 | } 253 | } 254 | help := item.help 255 | extra := strings.Join(outs, ", ") 256 | if extra != "" { 257 | if help == "" { 258 | help = extra 259 | } else if trailingBrackets.MatchString(help) { 260 | m := trailingBrackets.FindStringSubmatch(help) 261 | help = m[1] + "(" + m[2] + ", " + extra + ")" 262 | } else { 263 | help += " (" + extra + ")" 264 | } 265 | } 266 | help = constrain(help, helpWidth) 267 | //align each row after the flag 268 | lines := strings.Split(help, "\n") 269 | for i, l := range lines { 270 | if i > 0 { 271 | lines[i] = spaces + l 272 | } 273 | } 274 | to.Help = strings.Join(lines, "\n") 275 | } 276 | } 277 | //commands 278 | max = 0 279 | for _, s := range o.cmds { 280 | if l := len(s.name); l > max { 281 | max = l 282 | } 283 | } 284 | subs := make([]*datum, len(o.cmds)) 285 | i := 0 286 | cmdNames := []string{} 287 | for _, s := range o.cmds { 288 | cmdNames = append(cmdNames, s.name) 289 | } 290 | sort.Strings(cmdNames) 291 | for _, name := range cmdNames { 292 | s := o.cmds[name] 293 | h := s.help 294 | if h == "" { 295 | h = s.summary 296 | } 297 | explicitMatch := o.cmdname != nil && *o.cmdname == s.name 298 | envMatch := o.cmdnameEnv != "" && os.Getenv(o.cmdnameEnv) == s.name 299 | if explicitMatch || envMatch { 300 | if h == "" { 301 | h = "default" 302 | } else { 303 | h += " (default)" 304 | } 305 | } 306 | subs[i] = &datum{ 307 | Name: s.name, 308 | Help: h, 309 | Pad: nletters(' ', max-len(s.name)), 310 | } 311 | i++ 312 | } 313 | //convert error to string 314 | err := "" 315 | if o.err != nil { 316 | err = o.err.Error() 317 | } 318 | return &data{ 319 | datum: datum{ 320 | Name: name, 321 | Help: o.help, 322 | Pad: pad, 323 | }, 324 | Args: args, 325 | FlagGroups: flagGroups, 326 | Cmds: subs, 327 | Order: o.order, 328 | Version: o.version, 329 | Summary: constrain(o.summary, o.lineWidth), 330 | Repo: o.repo, 331 | Author: o.author, 332 | ErrMsg: err, 333 | }, nil 334 | } 335 | -------------------------------------------------------------------------------- /node_parse.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "reflect" 12 | "strconv" 13 | "strings" 14 | ) 15 | 16 | // Parse with os.Args 17 | func (n *node) Parse() ParsedOpts { 18 | return n.ParseArgs(os.Args) 19 | } 20 | 21 | // ParseArgs with the provided arguments and os.Exit on 22 | // any parse failure, or when handling shell completion. 23 | // Use ParseArgsError if you need to handle failure in 24 | // your application. 25 | func (n *node) ParseArgs(args []string) ParsedOpts { 26 | o, err := n.ParseArgsError(args) 27 | if err != nil { 28 | //expected user error, print message as-is 29 | if ee, ok := err.(exitError); ok { 30 | fmt.Fprint(os.Stderr, string(ee)) 31 | os.Exit(1) 32 | } 33 | //expected opts error, print message to programmer 34 | if ae, ok := err.(authorError); ok { 35 | fmt.Fprintf(os.Stderr, "opts usage error: %s\n", ae) 36 | os.Exit(1) 37 | } 38 | //unexpected exit (1) embed message in help to user 39 | fmt.Fprint(os.Stderr, n.Help()) 40 | os.Exit(1) 41 | } 42 | //success 43 | return o 44 | } 45 | 46 | // ParseArgsError with the provided arguments 47 | func (n *node) ParseArgsError(args []string) (ParsedOpts, error) { 48 | //shell-completion? 49 | if cl := os.Getenv("COMP_LINE"); n.complete && cl != "" { 50 | args := strings.Split(cl, " ") 51 | n.parse(args) //ignore error 52 | if ok := n.doCompletion(); !ok { 53 | os.Exit(1) 54 | } 55 | os.Exit(0) 56 | } 57 | //parse, storing any errors on the node itself 58 | if err := n.parse(args); err != nil { 59 | _, ee := err.(exitError) 60 | _, ae := err.(authorError) 61 | if !ee && !ae { 62 | n.err = err 63 | } 64 | return n, err 65 | } 66 | //success 67 | return n, nil 68 | } 69 | 70 | // parse validates and initialises all internal items 71 | // and then passes the args through, setting them items required 72 | func (n *node) parse(args []string) error { 73 | //return the stored error 74 | if n.err != nil { 75 | return n.err 76 | } 77 | //root node? take program from the arg list (assumes os.Args format) 78 | if n.parent == nil { 79 | prog := "" 80 | if len(args) > 0 { 81 | prog = args[0] 82 | args = args[1:] 83 | } 84 | //find default name for root-node 85 | if n.item.name == "" { 86 | if exe, err := os.Executable(); err == nil && exe != "" { 87 | //TODO: use filepath.EvalSymlinks first? 88 | _, n.item.name = path.Split(exe) 89 | } else if prog != "" { 90 | _, n.item.name = path.Split(prog) 91 | } 92 | //looks like weve been go-run, use package name? 93 | if n.item.name == "main" { 94 | if pkgPath := n.item.val.Type().PkgPath(); pkgPath != "" { 95 | _, n.item.name = path.Split(pkgPath) 96 | } 97 | } 98 | } 99 | } 100 | //add this node and its fields (recurses if has sub-commands) 101 | if err := n.addStructFields(defaultGroup, n.item.val); err != nil { 102 | return err 103 | } 104 | //add user provided flagsets, will error if there is a naming collision 105 | if err := n.addFlagsets(); err != nil { 106 | return err 107 | } 108 | //add help, version, etc flags 109 | if err := n.addInternalFlags(); err != nil { 110 | return err 111 | } 112 | //find defaults from config's package 113 | n.setPkgDefaults() 114 | //add shortnames where possible 115 | for _, item := range n.flags() { 116 | if !n.flagSkipShort[item.name] && item.shortName == "" && len(item.name) >= 2 { 117 | if s := item.name[0:1]; !n.flagNames[s] { 118 | item.shortName = s 119 | n.flagNames[s] = true 120 | } 121 | } 122 | } 123 | //create a new flagset, and link each item 124 | flagset := flag.NewFlagSet(n.item.name, flag.ContinueOnError) 125 | flagset.SetOutput(ioutil.Discard) 126 | for _, item := range n.flags() { 127 | flagset.Var(item, item.name, "") 128 | if sn := item.shortName; sn != "" && sn != "-" { 129 | flagset.Var(item, sn, "") 130 | } 131 | } 132 | if err := flagset.Parse(args); err != nil { 133 | //insert flag errors into help text 134 | n.err = err 135 | n.internalOpts.Help = true 136 | } 137 | //handle help, version, install/uninstall 138 | if n.internalOpts.Help { 139 | return exitError(n.Help()) 140 | } else if n.internalOpts.Version { 141 | return exitError(n.version) 142 | } else if n.internalOpts.Install { 143 | return n.manageCompletion(false) 144 | } else if n.internalOpts.Uninstall { 145 | return n.manageCompletion(true) 146 | } 147 | //first round of defaults, applying env variables where necessary 148 | for _, item := range n.flags() { 149 | k := item.envName 150 | if item.set() || k == "" { 151 | continue 152 | } 153 | v := os.Getenv(k) 154 | if v == "" { 155 | continue 156 | } 157 | err := item.Set(v) 158 | if err != nil { 159 | return fmt.Errorf("flag '%s' cannot set invalid env var (%s): %s", item.name, k, err) 160 | } 161 | } 162 | //second round, unmarshal directly into the struct, overwrites envs and flags 163 | if c := n.internalOpts.ConfigPath; c != "" { 164 | b, err := ioutil.ReadFile(c) 165 | if err == nil { 166 | v := n.val.Addr().Interface() //*struct 167 | err = json.Unmarshal(b, v) 168 | if err != nil { 169 | return fmt.Errorf("invalid config file: %s", err) 170 | } 171 | } 172 | } 173 | //get remaining args after extracting flags 174 | remaining := flagset.Args() 175 | i := 0 176 | for { 177 | if len(n.args) == i { 178 | break 179 | } 180 | item := n.args[i] 181 | if len(remaining) == 0 && !item.set() && !item.slice { 182 | return fmt.Errorf("argument '%s' is missing", item.name) 183 | } 184 | if len(remaining) == 0 { 185 | break 186 | } 187 | s := remaining[0] 188 | if err := item.Set(s); err != nil { 189 | return fmt.Errorf("argument '%s' is invalid: %s", item.name, err) 190 | } 191 | remaining = remaining[1:] 192 | //use next arg? 193 | if !item.slice { 194 | i++ 195 | } 196 | } 197 | //check min 198 | for _, item := range n.args { 199 | if item.slice && item.sets < item.min { 200 | return fmt.Errorf("argument '%s' has too few args (%d/%d)", item.name, item.sets, item.min) 201 | } 202 | if item.slice && item.max != 0 && item.sets > item.max { 203 | return fmt.Errorf("argument '%s' has too many args (%d/%d)", item.name, item.sets, item.max) 204 | } 205 | } 206 | //use command? next arg can optionally match command 207 | if len(n.cmds) > 0 { 208 | // use next arg as command 209 | args := remaining 210 | cmd := "" 211 | must := false 212 | if len(args) > 0 { 213 | cmd = args[0] 214 | args = args[1:] 215 | } 216 | // fallback to pre-initialised cmdname 217 | if cmd == "" { 218 | if n.cmdnameEnv != "" && os.Getenv(n.cmdnameEnv) != "" { 219 | cmd = os.Getenv(n.cmdnameEnv) 220 | } else if n.cmdname != nil && *n.cmdname != "" { 221 | cmd = *n.cmdname 222 | } 223 | must = true 224 | } 225 | //matching command 226 | if cmd != "" { 227 | sub, exists := n.cmds[cmd] 228 | if must && !exists { 229 | return fmt.Errorf("command '%s' does not exist", cmd) 230 | } 231 | if exists { 232 | //store matched command 233 | n.cmd = sub 234 | //user wants command name to be set on their struct? 235 | if n.cmdname != nil { 236 | *n.cmdname = cmd 237 | } 238 | //tail recurse! if only... 239 | return sub.parse(args) 240 | } 241 | } 242 | } 243 | //we *should* have consumed all args at this point. 244 | //this prevents: ./foo --bar 42 -z 21 ping --pong 7 245 | //where --pong 7 is ignored 246 | if len(remaining) != 0 { 247 | return fmt.Errorf("unexpected arguments: %s", strings.Join(remaining, " ")) 248 | } 249 | return nil 250 | } 251 | 252 | func (n *node) addStructFields(group string, sv reflect.Value) error { 253 | if sv.Kind() == reflect.Interface { 254 | sv = sv.Elem() 255 | } 256 | if sv.Kind() == reflect.Ptr { 257 | sv = sv.Elem() 258 | } 259 | if sv.Kind() != reflect.Struct { 260 | name := "" 261 | if sv.IsValid() { 262 | name = sv.Type().Name() 263 | } 264 | return n.errorf("opts: %s should be a pointer to a struct (got %s)", name, sv.Kind()) 265 | } 266 | for i := 0; i < sv.NumField(); i++ { 267 | sf := sv.Type().Field(i) 268 | val := sv.Field(i) 269 | if err := n.addStructField(group, sf, val); err != nil { 270 | return fmt.Errorf("field '%s' %s", sf.Name, err) 271 | } 272 | } 273 | return nil 274 | } 275 | 276 | func (n *node) addStructField(group string, sf reflect.StructField, val reflect.Value) error { 277 | kv := newKV(sf.Tag.Get("opts")) 278 | help := sf.Tag.Get("help") 279 | mode := sf.Tag.Get("type") //legacy versions of this package used "type" 280 | if m := sf.Tag.Get("mode"); m != "" { 281 | mode = m //allow "mode" to be used directly, undocumented! 282 | } 283 | if err := n.addKVField(kv, sf.Name, help, mode, group, val); err != nil { 284 | return err 285 | } 286 | if ks := kv.keys(); len(ks) > 0 { 287 | return fmt.Errorf("unused opts keys: %s", strings.Join(ks, ", ")) 288 | } 289 | return nil 290 | } 291 | 292 | func (n *node) addKVField(kv *kv, fName, help, mode, group string, val reflect.Value) error { 293 | //internal opts flag 294 | internal := kv == nil 295 | //ignore unaddressed/unexported fields 296 | if !val.CanSet() { 297 | return nil 298 | } 299 | //parse key-values 300 | //ignore `opts:"-"` 301 | if _, ok := kv.take("-"); ok { 302 | return nil 303 | } 304 | //get field name and mode 305 | name, _ := kv.take("name") 306 | if name == "" { 307 | //default to struct field name 308 | name = camel2dash(fName) 309 | //slice? use singular, usage of 310 | //Foos []string should be: --foo bar --foo bazz 311 | if val.Type().Kind() == reflect.Slice { 312 | name = getSingular(name) 313 | } 314 | } 315 | //new kv mode supercede legacy mode 316 | if t, ok := kv.take("mode"); ok { 317 | mode = t 318 | } 319 | //default opts mode from go type 320 | if mode == "" { 321 | switch val.Type().Kind() { 322 | case reflect.Struct: 323 | mode = "embedded" 324 | default: 325 | mode = "flag" 326 | } 327 | } 328 | //use the specified group 329 | if g, ok := kv.take("group"); ok { 330 | group = g 331 | } 332 | //special cases 333 | if mode == "embedded" { 334 | return n.addStructFields(group, val) //recurse! 335 | } 336 | //special cmdname to define a default command, or 337 | //to access the matched command name 338 | if mode == "cmdname" { 339 | if name, ok := kv.take("env"); ok { 340 | if name == "" { 341 | name = camel2const(fName) 342 | } 343 | n.cmdnameEnv = name 344 | } 345 | return n.setCmdName(val) 346 | } 347 | //new kv help defs supercede legacy defs 348 | if h, ok := kv.take("help"); ok { 349 | help = h 350 | } 351 | //inline sub-command 352 | if mode == "cmd" { 353 | return n.addInlineCmd(name, help, val) 354 | } 355 | //from this point, we must have a flag or an arg 356 | i, err := newItem(val) 357 | if err != nil { 358 | return err 359 | } 360 | i.mode = mode 361 | i.name = name 362 | i.help = help 363 | //insert either as flag or as argument 364 | switch mode { 365 | case "flag": 366 | //set default text 367 | if d, ok := kv.take("default"); ok { 368 | i.defstr = d 369 | } else if !i.slice { 370 | v := val.Interface() 371 | t := val.Type() 372 | z := reflect.Zero(t) 373 | zero := reflect.DeepEqual(v, z.Interface()) 374 | if !zero { 375 | i.defstr = fmt.Sprintf("%v", v) 376 | } 377 | } 378 | if e, ok := kv.take("env"); ok || n.useEnv { 379 | explicit := true 380 | if e == "" { 381 | explicit = false 382 | e = camel2const(i.name) 383 | } 384 | _, set := n.envNames[e] 385 | if set && explicit { 386 | return n.errorf("env name '%s' already in use", e) 387 | } 388 | if !internal && !set { 389 | n.envNames[e] = true 390 | i.envName = e 391 | i.useEnv = true 392 | } 393 | } 394 | //cannot have duplicates 395 | if _, ok := n.flagNames[name]; ok { 396 | return n.errorf("flag '%s' already exists", name) 397 | } 398 | //flags can also set short names 399 | if short, ok := kv.take("short"); ok { 400 | if short == "-" { 401 | n.flagSkipShort[name] = true 402 | } else if len(short) != 1 { 403 | return n.errorf("short name '%s' on flag '%s' must be a single character", short, name) 404 | } else if _, ok2 := n.flagNames[short]; ok2 { 405 | return n.errorf("short name '%s' on flag '%s' already exists", short, name) 406 | } else { 407 | n.flagNames[short] = true 408 | i.shortName = short 409 | } 410 | } 411 | //add to this command's flags 412 | n.flagNames[name] = true 413 | g := n.flagGroup(group) 414 | g.flags = append(g.flags, i) 415 | case "arg": 416 | //minimum number of items 417 | if i.slice { 418 | if m, ok := kv.take("min"); ok { 419 | min, err := strconv.Atoi(m) 420 | if err != nil { 421 | return n.errorf("min not an integer") 422 | } 423 | i.min = min 424 | } 425 | if m, ok := kv.take("max"); ok { 426 | max, err := strconv.Atoi(m) 427 | if err != nil { 428 | return n.errorf("max not an integer") 429 | } 430 | i.max = max 431 | } 432 | } 433 | //validations 434 | if group != "" { 435 | return n.errorf("args cannot be placed into a group") 436 | } 437 | if len(n.cmds) > 0 { 438 | return n.errorf("args and commands cannot be used together") 439 | } 440 | for _, item := range n.args { 441 | if item.slice { 442 | return n.errorf("cannot come after arg list '%s'", item.name) 443 | } 444 | } 445 | //add to this command's arguments 446 | n.args = append(n.args, i) 447 | default: 448 | return fmt.Errorf("invalid opts mode '%s'", mode) 449 | } 450 | return nil 451 | } 452 | 453 | func (n *node) setCmdName(val reflect.Value) error { 454 | if n.cmdname != nil { 455 | return n.errorf("cmdname set twice") 456 | } else if val.Type().Kind() != reflect.String { 457 | return n.errorf("cmdname type must be string") 458 | } else if !val.CanAddr() { 459 | return n.errorf("cannot address cmdname string") 460 | } 461 | n.cmdname = val.Addr().Interface().(*string) 462 | return nil 463 | } 464 | 465 | func (n *node) addInlineCmd(name, help string, val reflect.Value) error { 466 | vt := val.Type() 467 | if vt.Kind() == reflect.Ptr { 468 | vt = vt.Elem() 469 | } 470 | if vt.Kind() != reflect.Struct { 471 | return errors.New("inline commands 'type=cmd' must be structs") 472 | } else if !val.CanAddr() { 473 | return errors.New("cannot address inline command") 474 | } 475 | //if nil ptr, auto-create new struct 476 | if val.Kind() == reflect.Ptr && val.IsNil() { 477 | val.Set(reflect.New(vt)) 478 | } 479 | //ready! 480 | if _, ok := n.cmds[name]; ok { 481 | return n.errorf("command already exists: %s", name) 482 | } 483 | sub := newNode(val) 484 | sub.Name(name) 485 | sub.help = help 486 | sub.Summary(help) 487 | sub.parent = n 488 | n.cmds[name] = sub 489 | return nil 490 | } 491 | 492 | func (n *node) addInternalFlags() error { 493 | type internal struct{ name, help, group string } 494 | g := reflect.ValueOf(&n.internalOpts).Elem() 495 | flags := []internal{} 496 | if n.version != "" { 497 | flags = append(flags, 498 | internal{name: "Version", help: "display version"}, 499 | ) 500 | } 501 | flags = append(flags, 502 | internal{name: "Help", help: "display help"}, 503 | ) 504 | if n.complete { 505 | s := "shell" 506 | if bs := path.Base(os.Getenv("SHELL")); bs == "bash" || bs == "fish" || bs == "zsh" { 507 | s = bs 508 | } 509 | flags = append(flags, 510 | internal{name: "Install", help: "install " + s + "-completion", group: "Completion"}, 511 | internal{name: "Uninstall", help: "uninstall " + s + "-completion", group: "Completion"}, 512 | ) 513 | } 514 | if n.userCfgPath { 515 | flags = append(flags, 516 | internal{name: "ConfigPath", help: "path to a JSON file"}, 517 | ) 518 | } 519 | for _, i := range flags { 520 | sf, _ := g.Type().FieldByName(i.name) 521 | val := g.FieldByName(i.name) 522 | if err := n.addKVField(nil, sf.Name, i.help, "flag", i.group, val); err != nil { 523 | return fmt.Errorf("error adding internal flag: %s: %s", i.name, err) 524 | } 525 | } 526 | return nil 527 | } 528 | 529 | func (n *node) addFlagsets() error { 530 | //add provided flag sets 531 | for _, fs := range n.flagsets { 532 | var err error 533 | //add all flags in each set 534 | fs.VisitAll(func(f *flag.Flag) { 535 | //convert into item 536 | val := reflect.ValueOf(f.Value) 537 | i, er := newItem(val) 538 | if er != nil { 539 | err = n.errorf("imported flag '%s': %s", f.Name, er) 540 | return 541 | } 542 | i.name = f.Name 543 | i.defstr = f.DefValue 544 | i.help = f.Usage 545 | //cannot have duplicates 546 | if _, ok := n.flagNames[i.name]; ok { 547 | err = n.errorf("imported flag '%s' already exists", i.name) 548 | return 549 | } 550 | //ready! 551 | g := n.flagGroup("") 552 | g.flags = append(g.flags, i) 553 | n.flagNames[i.name] = true 554 | //convert f into a black hole 555 | f.Value = noopValue 556 | }) 557 | //fail with last error 558 | if err != nil { 559 | return err 560 | } 561 | fs.Init(fs.Name(), flag.ContinueOnError) 562 | fs.SetOutput(ioutil.Discard) 563 | fs.Parse([]string{}) //ensure this flagset returns Parsed() => true 564 | } 565 | return nil 566 | } 567 | 568 | func (n *node) setPkgDefaults() { 569 | //attempt to infer package name, repo, author 570 | configStruct := n.item.val.Type() 571 | pkgPath := configStruct.PkgPath() 572 | parts := strings.Split(pkgPath, "/") 573 | if len(parts) >= 3 { 574 | if n.authorInfer && n.author == "" { 575 | n.author = parts[1] 576 | } 577 | if n.repoInfer && n.repo == "" { 578 | switch parts[0] { 579 | case "github.com", "bitbucket.org": 580 | n.repo = "https://" + strings.Join(parts[0:3], "/") 581 | } 582 | } 583 | } 584 | } 585 | -------------------------------------------------------------------------------- /opts.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "flag" 5 | "reflect" 6 | ) 7 | 8 | //Opts is a single configuration command instance. It represents a node 9 | //in a tree of commands. Use the AddCommand method to add subcommands (child nodes) 10 | //to this command instance. 11 | type Opts interface { 12 | //Name of the command. For the root command, Name defaults to the executable's 13 | //base name. For subcommands, Name defaults to the package name, unless its the 14 | //main package, then it defaults to the struct name. 15 | Name(name string) Opts 16 | //Version of the command. Commonly set using a package main variable at compile 17 | //time using ldflags (for example, go build -ldflags -X main.version=42). 18 | Version(version string) Opts 19 | //ConfigPath is a path to a JSON file to use as defaults. This is useful in 20 | //global paths like /etc/my-prog.json. For a user-specified path. Use the 21 | //UserConfigPath method. 22 | ConfigPath(path string) Opts 23 | //UserConfigPath is the same as ConfigPath however an extra flag (--config-path) 24 | //is added to this Opts instance to give the user control of the filepath. 25 | //Configuration unmarshalling occurs after flag parsing. 26 | UserConfigPath() Opts 27 | //UseEnv enables the default environment variables on all fields. This is 28 | //equivalent to adding the opts tag "env" on all flag fields. 29 | UseEnv() Opts 30 | //Complete enables auto-completion for this command. When enabled, two extra 31 | //flags are added (--install and --uninstall) which can be used to install 32 | //a dynamic shell (bash, zsh, fish) completion for this command. Internally, 33 | //this adds a stub file which runs the Go binary to auto-complete its own 34 | //command-line interface. Note, the absolute path returned from os.Executable() 35 | //is used to reference to the Go binary. 36 | Complete() Opts 37 | //EmbedFlagSet embeds the given pkg/flag.FlagSet into 38 | //this Opts instance. Placing the flags defined in the FlagSet 39 | //along side the configuration struct flags. 40 | EmbedFlagSet(*flag.FlagSet) Opts 41 | //EmbedGlobalFlagSet embeds the global pkg/flag.CommandLine 42 | //FlagSet variable into this Opts instance. 43 | EmbedGlobalFlagSet() Opts 44 | 45 | //Summary adds a short sentence below the usage text 46 | Summary(summary string) Opts 47 | //Repo sets the source repository of the program and is displayed 48 | //at the bottom of the help text. 49 | Repo(repo string) Opts 50 | //Author sets the author of the program and is displayed 51 | //at the bottom of the help text. 52 | Author(author string) Opts 53 | //PkgRepo automatically sets Repo using the struct's package path. 54 | //This does not work for types defined in the main package. 55 | PkgRepo() Opts 56 | //PkgAuthor automatically sets Author using the struct's package path. 57 | //This does not work for types defined in the main package. 58 | PkgAuthor() Opts 59 | //DocSet replaces an existing template. 60 | DocSet(id, template string) Opts 61 | //DocBefore inserts a new template before an existing template. 62 | DocBefore(existingID, newID, template string) Opts 63 | //DocAfter inserts a new template after an existing template. 64 | DocAfter(existingID, newID, template string) Opts 65 | //DisablePadAll removes the padding from the help text. 66 | DisablePadAll() Opts 67 | //SetPadWidth alters the padding to specific number of spaces. 68 | //By default, pad width is 2. 69 | SetPadWidth(padding int) Opts 70 | //SetLineWidth alters the maximum number of characters in a 71 | //line (excluding padding). By default, line width is 96. 72 | SetLineWidth(width int) Opts 73 | 74 | //AddCommand adds another Opts instance as a subcommand. 75 | AddCommand(Opts) Opts 76 | //Parse calls ParseArgs(os.Args). 77 | Parse() ParsedOpts 78 | //ParseArgs parses the given strings and stores the results 79 | //in your provided struct. Assumes the executed program is 80 | //the first arg. Parse failures will call os.Exit. 81 | ParseArgs(args []string) ParsedOpts 82 | //ParseArgsError is the same as ParseArgs except you can 83 | //handle the error. 84 | ParseArgsError(args []string) (ParsedOpts, error) 85 | } 86 | 87 | type ParsedOpts interface { 88 | //Help returns the final help text 89 | Help() string 90 | //IsRunnable returns whether the matched command has a Run method 91 | IsRunnable() bool 92 | //Run assumes the matched command is runnable and executes its Run method. 93 | //The target Run method must be 'Run() error' or 'Run()' 94 | Run() error 95 | //RunFatal assumes the matched command is runnable and executes its Run method. 96 | //However, any error will be printed, followed by an exit(1). 97 | RunFatal() 98 | //Selected returns the subcommand picked when parsing the command line 99 | Selected() ParsedOpts 100 | } 101 | 102 | //New creates a new Opts instance using the given configuration 103 | //struct pointer. 104 | func New(config interface{}) Opts { 105 | return newNode(reflect.ValueOf(config)) 106 | } 107 | 108 | //Parse is shorthand for 109 | // opts.New(config).Parse() 110 | func Parse(config interface{}) ParsedOpts { 111 | return New(config).Parse() 112 | } 113 | 114 | //Setter is any type which can be set from a string. 115 | //This includes flag.Value. 116 | type Setter interface { 117 | Set(string) error 118 | } 119 | -------------------------------------------------------------------------------- /opts_test.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "regexp" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | func TestStrings(t *testing.T) { 16 | //config 17 | type Config struct { 18 | Foo string 19 | Bar string 20 | } 21 | c := &Config{} 22 | //flag example parse 23 | err := testNew(c).parse([]string{"/bin/prog", "--foo", "hello", "--bar", "world"}) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | //check config is filled 28 | check(t, c.Foo, "hello") 29 | check(t, c.Bar, "world") 30 | } 31 | 32 | func TestStrings2(t *testing.T) { 33 | //config 34 | type Config struct { 35 | Foo string 36 | Bar string 37 | } 38 | c := &Config{} 39 | //flag example parse 40 | err := testNew(c).parse([]string{"/bin/prog", "--foo", "hello", "--bar", "world with spaces"}) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | //check config is filled 45 | check(t, c.Foo, "hello") 46 | check(t, c.Bar, "world with spaces") 47 | } 48 | 49 | func TestStrings3(t *testing.T) { 50 | type MyString string 51 | //config 52 | type Config struct { 53 | Foo string 54 | Bar MyString 55 | } 56 | c := &Config{} 57 | //flag example parse 58 | err := testNew(c).parse([]string{"/bin/prog", "--foo", "hello", "--bar", "world with spaces"}) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | //check config is filled 63 | check(t, c.Foo, "hello") 64 | check(t, c.Bar, MyString("world with spaces")) 65 | } 66 | 67 | func TestList(t *testing.T) { 68 | //config 69 | type Config struct { 70 | Foo []string 71 | Bar []string 72 | } 73 | c := &Config{} 74 | //flag example parse 75 | err := testNew(c).parse([]string{"/bin/prog", "--foo", "hello", "--foo", "world", "--bar", "ping", "--bar", "pong"}) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | //check config is filled 80 | check(t, c.Foo, []string{"hello", "world"}) 81 | check(t, c.Bar, []string{"ping", "pong"}) 82 | } 83 | 84 | func TestBool(t *testing.T) { 85 | //config 86 | type Config struct { 87 | Foo string 88 | Bar bool 89 | } 90 | c := &Config{} 91 | //flag example parse 92 | err := testNew(c).parse([]string{"/bin/prog", "--foo", "hello", "--bar"}) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | //check config is filled 97 | check(t, c.Foo, "hello") 98 | check(t, c.Bar, true) 99 | } 100 | 101 | func TestSubCommand(t *testing.T) { 102 | //subconfig 103 | type FooConfig struct { 104 | Ping string 105 | Pong string 106 | } 107 | //config 108 | type Config struct { 109 | Cmd string `opts:"mode=cmdname"` 110 | //command (external struct) 111 | Foo FooConfig `opts:"mode=cmd"` 112 | //command (inline struct) 113 | Bar struct { 114 | Zip string 115 | Zap string 116 | } `opts:"mode=cmd"` 117 | } 118 | c := &Config{} 119 | err := testNew(c).parse([]string{"/bin/prog", "bar", "--zip", "hello", "--zap", "world"}) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | //check config is filled 124 | check(t, c.Cmd, "bar") 125 | check(t, c.Foo.Ping, "") 126 | check(t, c.Foo.Pong, "") 127 | check(t, c.Bar.Zip, "hello") 128 | check(t, c.Bar.Zap, "world") 129 | } 130 | 131 | func TestEmbed(t *testing.T) { 132 | type Foo struct { 133 | Ping string 134 | Pong string 135 | } 136 | type Bar struct { 137 | Zip string 138 | Zap string 139 | } 140 | //config 141 | type Config struct { 142 | Foo 143 | Bar 144 | } 145 | c := &Config{ 146 | Bar: Bar{ 147 | Zap: "default", 148 | }, 149 | } 150 | err := testNew(c).parse([]string{"/bin/prog", "--zip", "hello", "--pong", "world"}) 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | //check config is filled 155 | check(t, c.Bar.Zap, "default") 156 | check(t, c.Bar.Zip, "hello") 157 | check(t, c.Foo.Ping, "") 158 | check(t, c.Foo.Pong, "world") 159 | } 160 | 161 | func TestDefaultCommand(t *testing.T) { 162 | os.Setenv("CMD", "bar") 163 | os.Setenv("PING", "ignored") 164 | os.Setenv("ZAP", "helloworld") 165 | defer os.Unsetenv("CMD") 166 | defer os.Unsetenv("ZAP") 167 | type Foo struct { 168 | Ping string `opts:"env"` 169 | Pong string 170 | } 171 | type Bar struct { 172 | Zip string 173 | Zap string `opts:"env"` 174 | } 175 | //config 176 | type Config struct { 177 | Cmd string `opts:"mode=cmdname, env"` 178 | Foo `opts:"mode=cmd"` 179 | Bar `opts:"mode=cmd"` 180 | } 181 | c := &Config{ 182 | Bar: Bar{ 183 | Zap: "default", 184 | }, 185 | } 186 | //no args 187 | err := testNew(c).parse([]string{"/bin/prog"}) 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | //should default to "bar" and use env to fill Zap 192 | check(t, c.Cmd, "bar") 193 | check(t, c.Bar.Zap, "helloworld") 194 | check(t, c.Bar.Zip, "") 195 | check(t, c.Foo.Ping, "") 196 | check(t, c.Foo.Pong, "") 197 | } 198 | 199 | func TestUnsupportedType(t *testing.T) { 200 | //config 201 | type Config struct { 202 | Foo string 203 | Bar map[string]bool 204 | } 205 | c := Config{} 206 | //flag example parse 207 | err := testNew(&c).parse([]string{"/bin/prog", "--foo", "hello", "--bar", "world"}) 208 | if err == nil { 209 | t.Fatal("Expected error") 210 | } 211 | if !strings.Contains(err.Error(), "field type not supported: map") { 212 | t.Fatalf("Expected unsupported map, got: %s", err) 213 | } 214 | } 215 | 216 | func TestUnsupportedInterfaceType(t *testing.T) { 217 | //config 218 | type Config struct { 219 | Foo string 220 | Bar interface{} 221 | } 222 | c := Config{} 223 | //flag example parse 224 | err := testNew(&c).parse([]string{"/bin/prog", "--foo", "hello", "--bar", "world"}) 225 | if err == nil { 226 | t.Fatal("Expected error") 227 | } 228 | if !strings.Contains(err.Error(), "field type not supported: interface") { 229 | t.Fatalf("Expected unsupported interface, got: %s", err) 230 | } 231 | } 232 | 233 | func TestEnv(t *testing.T) { 234 | os.Setenv("STR", "helloworld") 235 | os.Setenv("NUM", "42") 236 | os.Setenv("BOOL", "true") 237 | os.Setenv("ANOTHER_NUM", "21") 238 | defer os.Unsetenv("STR") 239 | defer os.Unsetenv("NUM") 240 | defer os.Unsetenv("BOOL") 241 | //config 242 | type Config struct { 243 | Str string 244 | Num int 245 | Bool bool 246 | AnotherNum int64 247 | } 248 | c := &Config{} 249 | //flag example parse 250 | n := testNew(c) 251 | n.UseEnv() 252 | if err := n.parse([]string{}); err != nil { 253 | t.Fatal(err) 254 | } 255 | //check config is filled 256 | check(t, c.Str, `helloworld`) 257 | check(t, c.Num, 42) 258 | check(t, c.Bool, true) 259 | check(t, c.AnotherNum, int64(21)) 260 | } 261 | 262 | func TestLongClash(t *testing.T) { 263 | type Config struct { 264 | Foo string 265 | Fee string `opts:"name=foo"` 266 | } 267 | c := &Config{} 268 | //flag example parse 269 | n := testNew(c) 270 | if err := n.parse([]string{}); err == nil { 271 | t.Fatal("expected error") 272 | } else if !strings.Contains(err.Error(), "already exists") { 273 | t.Fatal("expected already exists error") 274 | } 275 | } 276 | 277 | func TestShortClash(t *testing.T) { 278 | type Config struct { 279 | Foo string `opts:"short=f"` 280 | Fee string `opts:"short=f"` 281 | } 282 | c := &Config{} 283 | //flag example parse 284 | n := testNew(c) 285 | if err := n.parse([]string{}); err == nil { 286 | t.Fatal("expected error") 287 | } else if !strings.Contains(err.Error(), "already exists") { 288 | t.Fatal("expected already exists error") 289 | } 290 | } 291 | 292 | func TestShortSkip(t *testing.T) { 293 | type Config struct { 294 | Foo string `opts:"short=f"` 295 | Bar string `opts:"short=-"` 296 | Lalala string 297 | } 298 | c := &Config{} 299 | o, _ := New(c).Version("1.2.3").Name("skipshort").ParseArgsError([]string{"/bin/prog", "--help"}) 300 | check(t, o.Help(), ` 301 | Usage: skipshort [options] 302 | 303 | Options: 304 | --foo, -f 305 | --bar 306 | --lalala, -l 307 | --version, -v display version 308 | --help, -h display help 309 | 310 | Version: 311 | 1.2.3 312 | 313 | `) 314 | } 315 | 316 | func TestShortSkipConflictHelp(t *testing.T) { 317 | type Config struct { 318 | Foo string `opts:"short=f"` 319 | Bar string `opts:"short=-"` 320 | Hahaha string 321 | } 322 | c := &Config{} 323 | o, _ := New(c).Version("1.2.3").Name("skipshort").ParseArgsError([]string{"/bin/prog", "--help"}) 324 | check(t, o.Help(), ` 325 | Usage: skipshort [options] 326 | 327 | Options: 328 | --foo, -f 329 | --bar 330 | --hahaha, -h 331 | --version, -v display version 332 | --help display help 333 | 334 | Version: 335 | 1.2.3 336 | 337 | `) 338 | } 339 | 340 | func TestShortSkipInternal(t *testing.T) { 341 | type Config struct { 342 | Foo string `opts:"short=f"` 343 | Bar string `opts:"short=-"` 344 | Hahaha string `opts:"short=-"` 345 | } 346 | c := &Config{} 347 | o, _ := New(c).Version("1.2.3").Name("skipshort").ParseArgsError([]string{"/bin/prog", "--help"}) 348 | check(t, o.Help(), ` 349 | Usage: skipshort [options] 350 | 351 | Options: 352 | --foo, -f 353 | --bar 354 | --hahaha 355 | --version, -v display version 356 | --help, -h display help 357 | 358 | Version: 359 | 1.2.3 360 | 361 | `) 362 | } 363 | 364 | func TestJSON(t *testing.T) { 365 | //insert a config file 366 | p := filepath.Join(os.TempDir(), "opts.json") 367 | b := []byte(`{"foo":"hello", "bar":7}`) 368 | if err := ioutil.WriteFile(p, b, 0755); err != nil { 369 | t.Fatal(err) 370 | } 371 | defer os.Remove(p) 372 | //parse flags 373 | type Config struct { 374 | Foo string 375 | Bar int 376 | } 377 | c := &Config{} 378 | //flag example parse 379 | n := testNew(c) 380 | n.ConfigPath(p) 381 | if err := n.parse([]string{"/bin/prog", "--bar", "8"}); err != nil { 382 | t.Fatal(err) 383 | } 384 | check(t, c.Foo, `hello`) 385 | check(t, c.Bar, 7) //currently uses JSON value... might change... 386 | } 387 | 388 | func TestArg(t *testing.T) { 389 | //config 390 | type Config struct { 391 | Foo string `opts:"mode=arg"` 392 | Zip string `opts:"mode=arg"` 393 | Bar string 394 | } 395 | c := &Config{} 396 | //flag example parse 397 | if err := testNew(c).parse([]string{"/bin/prog", "-b", "wld", "hel", "lo"}); err != nil { 398 | t.Fatal(err) 399 | } 400 | //check config is filled 401 | check(t, c.Foo, `hel`) 402 | check(t, c.Zip, `lo`) 403 | check(t, c.Bar, `wld`) 404 | } 405 | 406 | func TestArgs(t *testing.T) { 407 | //config 408 | type Config struct { 409 | Zip string `opts:"mode=arg"` 410 | Foo []string `opts:"mode=arg"` 411 | Bar string 412 | } 413 | c := &Config{} 414 | //flag example parse 415 | if err := testNew(c).parse([]string{"/bin/prog", "-b", "wld", "!!!", "hel", "lo"}); err != nil { 416 | t.Fatal(err) 417 | } 418 | //check config is filled 419 | check(t, c.Zip, `!!!`) 420 | check(t, c.Foo, []string{`hel`, `lo`}) 421 | check(t, c.Bar, `wld`) 422 | } 423 | 424 | func TestIgnoreUnexported(t *testing.T) { 425 | //config 426 | type Config struct { 427 | Foo string 428 | bar string 429 | } 430 | c := &Config{} 431 | //flag example parse 432 | err := testNew(c).parse([]string{"/bin/prog", "-f", "1", "-b", "2"}) 433 | if err == nil { 434 | t.Fatal("expected error") 435 | } 436 | } 437 | 438 | func TestDocBefore(t *testing.T) { 439 | //config 440 | type Config struct { 441 | Foo string 442 | bar string 443 | } 444 | c := &Config{} 445 | //flag example parse 446 | o := New(c).Name("doc-before") 447 | n := o.(*node) 448 | l := len(n.order) 449 | o.DocBefore("usage", "mypara", "hello world this some text\n\n") 450 | op := o.ParseArgs(nil) 451 | check(t, len(n.order), l+1) 452 | check(t, op.Help(), ` 453 | hello world this some text 454 | 455 | Usage: doc-before [options] 456 | 457 | Options: 458 | --foo, -f 459 | --help, -h display help 460 | 461 | `) 462 | } 463 | 464 | func TestDocAfter(t *testing.T) { 465 | //config 466 | type Config struct { 467 | Foo string 468 | bar string 469 | } 470 | c := &Config{} 471 | //flag example parse 472 | o := New(c).Name("doc-after") 473 | n := o.(*node) 474 | l := len(n.order) 475 | o.DocAfter("usage", "mypara", "\nhello world this some text\n") 476 | op := o.ParseArgs(nil) 477 | check(t, len(n.order), l+1) 478 | check(t, op.Help(), ` 479 | Usage: doc-after [options] 480 | 481 | hello world this some text 482 | 483 | Options: 484 | --foo, -f 485 | --help, -h display help 486 | 487 | `) 488 | } 489 | 490 | func TestDocGroups(t *testing.T) { 491 | //config 492 | type Config struct { 493 | Fizz string 494 | Buzz bool 495 | Ping, Pong int `opts:"group=More"` 496 | } 497 | c := &Config{} 498 | //flag example parse 499 | o := New(c).Name("groups").ParseArgs(nil) 500 | check(t, o.Help(), ` 501 | Usage: groups [options] 502 | 503 | Options: 504 | --fizz, -f 505 | --buzz, -b 506 | --help, -h display help 507 | 508 | More options: 509 | --ping, -p 510 | --pong 511 | 512 | `) 513 | } 514 | 515 | func TestDocArgList(t *testing.T) { 516 | //config 517 | type Config struct { 518 | Foo string `opts:"mode=arg"` 519 | Bar []string `opts:"mode=arg"` 520 | } 521 | c := &Config{} 522 | //flag example parse 523 | o := New(c).Name("docargs").ParseArgs([]string{"/bin/prog", "zzz"}) 524 | check(t, o.Help(), ` 525 | Usage: docargs [options] [bar] [bar] ... 526 | 527 | Options: 528 | --help, -h display help 529 | 530 | `) 531 | } 532 | 533 | func TestDocBrackets(t *testing.T) { 534 | //config 535 | type Config struct { 536 | Foo string `opts:"help=a message (submessage)"` 537 | } 538 | c := &Config{ 539 | Foo: "bar", 540 | } 541 | //flag example parse 542 | o, _ := New(c).Name("docbrackets").ParseArgsError([]string{"/bin/prog", "--help"}) 543 | check(t, o.Help(), ` 544 | Usage: docbrackets [options] 545 | 546 | Options: 547 | --foo, -f a message (submessage, default bar) 548 | --help, -h display help 549 | 550 | `) 551 | } 552 | 553 | func TestDocUseEnv(t *testing.T) { 554 | //config 555 | type Config struct { 556 | Foo string `opts:"help=a message"` 557 | } 558 | c := &Config{} 559 | //flag example parse 560 | o, _ := New(c).UseEnv().Version("1.2.3").Name("docuseenv").ParseArgsError([]string{"/bin/prog", "--help"}) 561 | check(t, o.Help(), ` 562 | Usage: docuseenv [options] 563 | 564 | Options: 565 | --foo, -f a message (env FOO) 566 | --version, -v display version 567 | --help, -h display help 568 | 569 | Version: 570 | 1.2.3 571 | 572 | `) 573 | } 574 | 575 | func TestCustomFlags(t *testing.T) { 576 | //config 577 | type Config struct { 578 | Foo *url.URL `opts:"help=my url"` 579 | Bar *url.URL `opts:"help=another url"` 580 | } 581 | c := Config{ 582 | Foo: &url.URL{}, 583 | } 584 | //flag example parse 585 | n := testNew(&c) 586 | if err := n.parse([]string{"/bin/prog", "-f", "http://foo.com"}); err != nil { 587 | t.Fatal(err) 588 | } 589 | if c.Foo == nil || c.Foo.String() != "http://foo.com" { 590 | t.Fatalf("incorrect foo: %v", c.Foo) 591 | } 592 | if c.Bar != nil { 593 | t.Fatal("bar should be nil") 594 | } 595 | } 596 | 597 | var spaces = regexp.MustCompile(`\ `) 598 | var newlines = regexp.MustCompile(`\n`) 599 | 600 | func readable(s string) string { 601 | s = spaces.ReplaceAllString(s, "•") 602 | s = newlines.ReplaceAllString(s, "⏎\n") 603 | lines := strings.Split(s, "\n") 604 | for i, l := range lines { 605 | lines[i] = fmt.Sprintf("%5d: %s", i+1, l) 606 | } 607 | s = strings.Join(lines, "\n") 608 | return s 609 | } 610 | 611 | func check(t *testing.T, a, b interface{}) { 612 | if !reflect.DeepEqual(a, b) { 613 | stra := readable(fmt.Sprintf("%v", a)) 614 | strb := readable(fmt.Sprintf("%v", b)) 615 | typea := reflect.ValueOf(a) 616 | typeb := reflect.ValueOf(b) 617 | extra := "" 618 | if out, ok := diffstr(stra, strb); ok { 619 | extra = "\n\n" + out 620 | stra = "\n" + stra + "\n" 621 | strb = "\n" + strb + "\n" 622 | } else { 623 | stra = "'" + stra + "'" 624 | strb = "'" + strb + "'" 625 | } 626 | t.Fatalf("got %s (%s), expected %s (%s)%s", stra, typea.Type(), strb, typeb.Type(), extra) 627 | } 628 | } 629 | 630 | func diffstr(a, b interface{}) (string, bool) { 631 | stra, oka := a.(string) 632 | strb, okb := b.(string) 633 | if !oka || !okb { 634 | return "", false 635 | } 636 | ra := []rune(stra) 637 | rb := []rune(strb) 638 | line := 1 639 | char := 1 640 | var diff rune 641 | for i, a := range ra { 642 | if a == '\n' { 643 | line++ 644 | char = 1 645 | } else { 646 | char++ 647 | } 648 | var b rune 649 | if i < len(rb) { 650 | b = rb[i] 651 | } 652 | if a != b { 653 | a = diff 654 | break 655 | } 656 | } 657 | return fmt.Sprintf("Diff on line %d char %d (%d)", line, char, diff), true 658 | } 659 | 660 | func testNew(config interface{}) *node { 661 | o := New(config) 662 | n := o.(*node) 663 | return n 664 | } 665 | -------------------------------------------------------------------------------- /strings.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "bytes" 5 | "regexp" 6 | "sort" 7 | "strings" 8 | "unicode" 9 | "unicode/utf8" 10 | ) 11 | 12 | func camel2const(s string) string { 13 | b := strings.Builder{} 14 | var c rune 15 | start := 0 16 | end := 0 17 | for end, c = range s { 18 | if c >= 'A' && c <= 'Z' { 19 | //uppercase all prior letters and add an underscore 20 | if start < end { 21 | b.WriteString(strings.ToTitle(s[start:end] + "_")) 22 | start = end 23 | } 24 | } 25 | } 26 | //write remaining string 27 | b.WriteString(strings.ToTitle(s[start : end+1])) 28 | return strings.ReplaceAll(b.String(), "-", "_") 29 | } 30 | 31 | func nletters(r rune, n int) string { 32 | str := make([]rune, n) 33 | for i := range str { 34 | str[i] = r 35 | } 36 | return string(str) 37 | } 38 | 39 | func constrain(str string, maxWidth int) string { 40 | lines := strings.Split(str, "\n") 41 | for i, line := range lines { 42 | words := strings.Split(line, " ") 43 | width := 0 44 | for i, w := range words { 45 | remain := maxWidth - width 46 | wordWidth := len(w) + 1 //+space 47 | width += wordWidth 48 | overflow := width > maxWidth 49 | fits := width-maxWidth > remain 50 | if overflow && fits { 51 | width = wordWidth 52 | w = "\n" + w 53 | } 54 | words[i] = w 55 | } 56 | lines[i] = strings.Join(words, " ") 57 | } 58 | return strings.Join(lines, "\n") 59 | } 60 | 61 | //borrowed from https://github.com/huandu/xstrings/blob/master/convert.go#L77 62 | func camel2dash(str string) string { 63 | if len(str) == 0 { 64 | return "" 65 | } 66 | buf := &bytes.Buffer{} 67 | var prev, r0, r1 rune 68 | var size int 69 | r0 = '-' 70 | for len(str) > 0 { 71 | prev = r0 72 | r0, size = utf8.DecodeRuneInString(str) 73 | str = str[size:] 74 | switch { 75 | case r0 == utf8.RuneError: 76 | buf.WriteByte(byte(str[0])) 77 | case unicode.IsUpper(r0): 78 | if prev != '-' { 79 | buf.WriteRune('-') 80 | } 81 | buf.WriteRune(unicode.ToLower(r0)) 82 | if len(str) == 0 { 83 | break 84 | } 85 | r0, size = utf8.DecodeRuneInString(str) 86 | str = str[size:] 87 | if !unicode.IsUpper(r0) { 88 | buf.WriteRune(r0) 89 | break 90 | } 91 | // find next non-upper-case character and insert `_` properly. 92 | // it's designed to convert `HTTPServer` to `http_server`. 93 | // if there are more than 2 adjacent upper case characters in a word, 94 | // treat them as an abbreviation plus a normal word. 95 | for len(str) > 0 { 96 | r1 = r0 97 | r0, size = utf8.DecodeRuneInString(str) 98 | str = str[size:] 99 | if r0 == utf8.RuneError { 100 | buf.WriteRune(unicode.ToLower(r1)) 101 | buf.WriteByte(byte(str[0])) 102 | break 103 | } 104 | if !unicode.IsUpper(r0) { 105 | if r0 == '-' || r0 == ' ' || r0 == '_' { 106 | r0 = '-' 107 | buf.WriteRune(unicode.ToLower(r1)) 108 | } else { 109 | buf.WriteRune('-') 110 | buf.WriteRune(unicode.ToLower(r1)) 111 | buf.WriteRune(r0) 112 | } 113 | break 114 | } 115 | buf.WriteRune(unicode.ToLower(r1)) 116 | } 117 | if len(str) == 0 || r0 == '-' { 118 | buf.WriteRune(unicode.ToLower(r0)) 119 | break 120 | } 121 | default: 122 | if r0 == ' ' || r0 == '_' { 123 | r0 = '-' 124 | } 125 | buf.WriteRune(r0) 126 | } 127 | } 128 | return buf.String() 129 | } 130 | 131 | func camel2title(str string) string { 132 | dash := camel2dash(str) 133 | title := []rune(dash) 134 | for i, r := range title { 135 | if r == '-' { 136 | r = ' ' 137 | } 138 | if i == 0 { 139 | r = unicode.ToUpper(r) 140 | } 141 | title[i] = r 142 | } 143 | return string(title) 144 | } 145 | 146 | //borrowed from https://raw.githubusercontent.com/jinzhu/inflection/master/inflections.go 147 | var getSingular = func() func(str string) string { 148 | type inflection struct { 149 | regexp *regexp.Regexp 150 | replace string 151 | } 152 | // Regular is a regexp find replace inflection 153 | type Regular struct { 154 | find string 155 | replace string 156 | } 157 | // Irregular is a hard replace inflection, 158 | // containing both singular and plural forms 159 | type Irregular struct { 160 | singular string 161 | plural string 162 | } 163 | var singularInflections = []Regular{ 164 | {"s$", ""}, 165 | {"(ss)$", "${1}"}, 166 | {"(n)ews$", "${1}ews"}, 167 | {"([ti])a$", "${1}um"}, 168 | {"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$", "${1}sis"}, 169 | {"(^analy)(sis|ses)$", "${1}sis"}, 170 | {"([^f])ves$", "${1}fe"}, 171 | {"(hive)s$", "${1}"}, 172 | {"(tive)s$", "${1}"}, 173 | {"([lr])ves$", "${1}f"}, 174 | {"([^aeiouy]|qu)ies$", "${1}y"}, 175 | {"(s)eries$", "${1}eries"}, 176 | {"(m)ovies$", "${1}ovie"}, 177 | {"(c)ookies$", "${1}ookie"}, 178 | {"(x|ch|ss|sh)es$", "${1}"}, 179 | {"^(m|l)ice$", "${1}ouse"}, 180 | {"(bus)(es)?$", "${1}"}, 181 | {"(o)es$", "${1}"}, 182 | {"(shoe)s$", "${1}"}, 183 | {"(cris|test)(is|es)$", "${1}is"}, 184 | {"^(a)x[ie]s$", "${1}xis"}, 185 | {"(octop|vir)(us|i)$", "${1}us"}, 186 | {"(alias|status)(es)?$", "${1}"}, 187 | {"^(ox)en", "${1}"}, 188 | {"(vert|ind)ices$", "${1}ex"}, 189 | {"(matr)ices$", "${1}ix"}, 190 | {"(quiz)zes$", "${1}"}, 191 | {"(database)s$", "${1}"}, 192 | } 193 | var irregularInflections = []Irregular{ 194 | {"person", "people"}, 195 | {"man", "men"}, 196 | {"child", "children"}, 197 | {"sex", "sexes"}, 198 | {"move", "moves"}, 199 | {"mombie", "mombies"}, 200 | } 201 | var uncountableInflections = []string{"equipment", "information", "rice", "money", "species", "series", "fish", "sheep", "jeans", "police"} 202 | var compiledSingularMaps []inflection 203 | compiledSingularMaps = []inflection{} 204 | for _, uncountable := range uncountableInflections { 205 | inf := inflection{ 206 | regexp: regexp.MustCompile("^(?i)(" + uncountable + ")$"), 207 | replace: "${1}", 208 | } 209 | compiledSingularMaps = append(compiledSingularMaps, inf) 210 | } 211 | for _, value := range irregularInflections { 212 | infs := []inflection{ 213 | inflection{regexp: regexp.MustCompile(strings.ToUpper(value.plural) + "$"), replace: strings.ToUpper(value.singular)}, 214 | inflection{regexp: regexp.MustCompile(strings.Title(value.plural) + "$"), replace: strings.Title(value.singular)}, 215 | inflection{regexp: regexp.MustCompile(value.plural + "$"), replace: value.singular}, 216 | } 217 | compiledSingularMaps = append(compiledSingularMaps, infs...) 218 | } 219 | for i := len(singularInflections) - 1; i >= 0; i-- { 220 | value := singularInflections[i] 221 | infs := []inflection{ 222 | inflection{regexp: regexp.MustCompile(strings.ToUpper(value.find)), replace: strings.ToUpper(value.replace)}, 223 | inflection{regexp: regexp.MustCompile(value.find), replace: value.replace}, 224 | inflection{regexp: regexp.MustCompile("(?i)" + value.find), replace: value.replace}, 225 | } 226 | compiledSingularMaps = append(compiledSingularMaps, infs...) 227 | } 228 | return func(str string) string { 229 | for _, inflection := range compiledSingularMaps { 230 | if inflection.regexp.MatchString(str) { 231 | return inflection.regexp.ReplaceAllString(str, inflection.replace) 232 | } 233 | } 234 | return str 235 | } 236 | }() 237 | 238 | type kv struct { 239 | m map[string]string 240 | } 241 | 242 | func (kv *kv) keys() []string { 243 | if kv == nil { 244 | return nil 245 | } 246 | ks := []string{} 247 | for k := range kv.m { 248 | ks = append(ks, k) 249 | } 250 | sort.Strings(ks) 251 | return ks 252 | } 253 | 254 | func (kv *kv) take(k string) (string, bool) { 255 | if kv == nil { 256 | return "", false 257 | } 258 | v, ok := kv.m[k] 259 | if ok { 260 | delete(kv.m, k) 261 | } 262 | return v, ok 263 | } 264 | 265 | func newKV(s string) *kv { 266 | m := map[string]string{} 267 | key := "" 268 | keying := true 269 | sb := strings.Builder{} 270 | commit := func() { 271 | s := sb.String() 272 | if key == "" && s == "" { 273 | return 274 | } else if key == "" { 275 | m[s] = "" 276 | } else { 277 | m[key] = s 278 | key = "" 279 | } 280 | sb.Reset() 281 | } 282 | for _, r := range s { 283 | //key done 284 | if keying && sb.Len() == 0 && r == ' ' { 285 | continue //drop leading spaces 286 | } 287 | if keying && r == '=' { 288 | key = sb.String() 289 | sb.Reset() 290 | keying = false 291 | continue 292 | } 293 | //go to next 294 | if r == ',' { 295 | commit() 296 | keying = true 297 | continue 298 | } 299 | //write to builder 300 | sb.WriteRune(r) 301 | } 302 | //write last key=value 303 | commit() 304 | return &kv{m: m} 305 | } 306 | -------------------------------------------------------------------------------- /strings_test.go: -------------------------------------------------------------------------------- 1 | package opts 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestKVMap(t *testing.T) { 9 | for _, testcase := range []struct { 10 | input string 11 | output map[string]string 12 | }{ 13 | { 14 | "a=b,c=d", 15 | map[string]string{"a": "b", "c": "d"}, 16 | }, 17 | { 18 | "foo,,bar,,", 19 | map[string]string{"foo": "", "bar": ""}, 20 | }, 21 | { 22 | "ping=,,pong==,,", 23 | map[string]string{"ping": "", "pong": "="}, 24 | }, 25 | { 26 | "nospace=,, leadingspace==, trailingspace ,", 27 | map[string]string{"nospace": "", "leadingspace": "=", "trailingspace ": ""}, 28 | }, 29 | } { 30 | kv := newKV(testcase.input) 31 | m := kv.m 32 | if !reflect.DeepEqual(m, testcase.output) { 33 | t.Fatalf("input: %s\n expected: %s\n got: %s", 34 | testcase.input, 35 | testcase.output, 36 | m, 37 | ) 38 | } 39 | } 40 | } 41 | 42 | func TestCamel2Dash(t *testing.T) { 43 | for _, testcase := range []struct { 44 | input string 45 | output string 46 | }{ 47 | { 48 | "fooBar", 49 | "foo-bar", 50 | }, 51 | { 52 | "WordACRONYMAnotherWord", 53 | "word-acronym-another-word", 54 | }, 55 | } { 56 | got := camel2dash(testcase.input) 57 | if testcase.output != got { 58 | t.Fatalf("input: %s\n expected: %s\n got: %s", 59 | testcase.input, 60 | testcase.output, 61 | got, 62 | ) 63 | } 64 | } 65 | 66 | } 67 | --------------------------------------------------------------------------------