├── .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 | 
3 | A Go (golang) package for building frictionless command-line interfaces
4 |
5 |
6 |
7 |
8 |
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 | [](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 |
--------------------------------------------------------------------------------