pax_global_header00006660000000000000000000000064147420222370014515gustar00rootroot0000000000000052 comment=d94aa2044dff005d95c599a36bb7883ff508c743 golang-sourcehut-rjarry-go-opt-2.0.1/000077500000000000000000000000001474202223700175155ustar00rootroot00000000000000golang-sourcehut-rjarry-go-opt-2.0.1/.build.yml000066400000000000000000000004561474202223700214220ustar00rootroot00000000000000image: alpine/edge packages: - go sources: - https://git.sr.ht/~rjarry/go-opt artifacts: - go-opt/coverage.html tasks: - build: | cd go-opt go build -v ./... go test -v -coverprofile=coverage.txt -covermode=atomic ./... go tool cover -html=coverage.txt -o coverage.html golang-sourcehut-rjarry-go-opt-2.0.1/.gitignore000066400000000000000000000000141474202223700215000ustar00rootroot00000000000000/coverage.* golang-sourcehut-rjarry-go-opt-2.0.1/LICENSE000066400000000000000000000020601474202223700205200ustar00rootroot00000000000000Copyright (c) 2023 Robin Jarry The MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-sourcehut-rjarry-go-opt-2.0.1/README.md000066400000000000000000000161161474202223700210010ustar00rootroot00000000000000# go-opt [![builds.sr.ht status](https://builds.sr.ht/~rjarry/go-opt.svg)](https://builds.sr.ht/~rjarry/go-opt) [go-opt](https://git.sr.ht/~rjarry/go-opt) is a library to parse command line arguments based on tag annotations on struct fields. It came as a spin-off from [aerc](https://git.sr.ht/~rjarry/aerc) to deal with its internal commands. This project is a scaled down version of [go-arg](https://github.com/alexflint/go-arg) with different usage patterns in mind: command parsing and argument completion for internal application commands. ## License [MIT](https://git.sr.ht/~rjarry/go-opt/tree/main/item/LICENSE) The `shlex.go` file has been inspired from the [github.com/google/shlex]( https://github.com/google/shlex/blob/master/shlex.go) package which is licensed under the Apache 2.0 license. ## Contributing Set patches via email to [~rjarry/public-inbox@lists.sr.ht](mailto:~rjarry/aerc-devel@lists.sr.ht) or alternatively to [~rjarry/aerc-devel@lists.sr.ht](mailto:~rjarry/aerc-devel@lists.sr.ht) with the `PATCH go-opt` subject prefix. ```sh git config format.subjectPrefix "PATCH go-opt" git config sendemail.to "~rjarry/public-inbox@lists.sr.ht" ``` ## Usage ### Shell command line splitting ```go package main import ( "log" "git.sr.ht/~rjarry/go-opt/v2" ) func main() { args, err := opt.LexArgs(`foo 'bar baz' -f\ boz -- " yolo "`) if err != nil { log.Fatalf("error: %s\n", err) } fmt.Printf("count: %d\n", args.Count()) fmt.Printf("args: %#v\n", args.Args()) fmt.Printf("raw: %q\n", args.String()) fmt.Println("shift 2") args.Shift(2) fmt.Printf("count: %d\n", args.Count()) fmt.Printf("args: %#v\n", args.Args()) fmt.Printf("raw: %q\n", args.String()) } ``` ```console $ go run main.go count: 5 args: []string{"foo", "bar baz", "-f boz", "--", " yolo "} raw: "foo 'bar baz' -f\\ boz -- \" yolo \"" shift 2 count: 3 args: []string{"-f boz", "--", " yolo "} raw: "-f\\ boz -- \" yolo \"" ``` ### Argument parsing ```go package main import ( "fmt" "log" "git.sr.ht/~rjarry/go-opt/v2" ) type Foo struct { Delay time.Duration `opt:"-t,--delay" action:"ParseDelay" default:"1s"` Force bool `opt:"--force"` Name string `opt:"name" required:"false" metavar:"FOO"` Cmd []string `opt:"..."` } func (f *Foo) ParseDelay(arg string) error { d, err := time.ParseDuration(arg) if err != nil { return err } f.Delay = d return nil } func main() { var foo Foo err := opt.CmdlineToStruct("foo -f bar baz 'xy z' ", &foo) if err != nil { log.Fatalf("error: %s\n", err) } fmt.Printf("%#v\n", foo) } ``` ```console $ foo --force bar baz 'xy z' main.Foo{Delay:1000000000, Force:true, Name:"bar", Cmd:[]string{"baz", "xy z"}} $ foo -t error: -t takes a value. Usage: foo [-t ] [--force] [] ... ``` ### Argument completion ```go package main import ( "fmt" "log" "os" "strings" "git.sr.ht/~rjarry/go-opt/v2" ) type CompleteStruct struct { Name string `opt:"-n,--name" required:"true" complete:"CompleteName"` Delay float64 `opt:"--delay"` Zero bool `opt:"-z"` Backoff bool `opt:"-B,--backoff"` Tags []string `opt:"..." complete:"CompleteTag"` } func (c *CompleteStruct) CompleteName(arg string) []string { return []string{"leonardo", "michelangelo", "rafaelo", "donatello"} } func (c *CompleteStruct) CompleteTag(arg string) []string { var results []string prefix := "" if strings.HasPrefix(arg, "-") { prefix = "-" } else if strings.HasPrefix(arg, "+") { prefix = "+" } tags := []string{"unread", "sent", "important", "inbox", "trash"} for _, t := range tags { t = prefix + t if strings.HasPrefix(t, arg) { results = append(results, t) } } return results } func main() { args, err := opt.QuoteArgs(os.Args...) if err != nil { log.Fatalf("error: %s\n", err) } var s CompleteStruct completions, _ := opt.GetCompletions(args.String(), &s) for _, c := range completions { fmt.Println(c.Value) } } ``` ```console $ foo i important inbox $ foo - --backoff --delay --name -B -important -inbox -n -sent -trash -unread -z $ foo + +important +inbox +sent +trash +unread ``` ## Supported tags There is a set of tags that can be set on struct fields: ### `opt:"-f,--foo"` Registers that this field is associated to the specified flag(s). Unless a custom `action` method is specified, the flag value will be automatically converted from string to the field type (only basic scalar types are supported: all integers (both signed and unsigned), all floats and strings. If the field type is `bool`, the flag will take no value. ### `opt:"blah"` Field is associated to a positional argument. ### `opt:"..."` Field will be mapped to all remaining arguments. If the field is `string`, the raw command line will be stored preserving any shell quoting, otherwise the field needs to be `[]string` and will receive the remaining arguments after interpreting shell quotes. ### `opt:"-"` Special value to indicate that this command will accept any argument without any check nor parsing. The field on which it is set will not be updated and should be called `Unused struct{}` as a convention. The field name must start with an upper case to avoid linter warnings because of unused fields. ### `action:"ParseFoo"` Custom method to be used instead of the default automatic conversion. Needs to be a method with a pointer receiver to the struct itself, takes a single `string` argument and may return an `error` to abort parsing. The `action` method is responsible of updating the struct. ### `description:"foobaz"` or `desc:"foobaz"` A description that is returned alongside arguments during autocompletion. ### `default:"foobaz"` Default `string` value if not specified by the user. Will be processed by the same conversion/parsing as any other argument. ### `metavar:"foo|bar|baz"` Displayed name for argument values in the generated usage help. ### `required:"true|false"` By default, flag arguments are optional and positional arguments are required. Using this tag allows changing that default behaviour. If an argument is not required, it will be surrounded by square brackets in the generated usage help. ### `aliases:"cmd1,cmd2"` By default, arguments are interpreted for all command aliases. If this is specified, this field/option will only be applicable to the specified command aliases. ### `complete:"CompleteFoo"` Custom method to return the valid completions for the annotated option / argument. Needs to be a method with a pointer receiver to the struct itself, takes a single `string` argument and must return a `[]string` slice containing the valid completions. ## Caveats Depending on field types, the argument string values are parsed using the appropriate conversion function. If no `opt` tag is set on a field, it will be excluded from automated argument parsing. It can still be updated indirectly via a custom `action` method. Short flags can be combined like with `getopt(3)`: * Flags with no value: `-abc` is equivalent to `-a -b -c` * Flags with a value (options): `-j9` is equivalent to `-j 9` The special argument `--` forces an end to the flag parsing. The remaining arguments are interpreted as positional arguments (see `getopt(3)`). golang-sourcehut-rjarry-go-opt-2.0.1/args.go000066400000000000000000000123721474202223700210050ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2023 Robin Jarry package opt import ( "errors" "strings" ) // Shell command line with interpreted arguments. // Allows access to individual arguments and to preserve shell quoting. type Args struct { raw []rune infos []argInfo } // Interpret a shell command line into multiple arguments. func LexArgs(cmd string) *Args { raw := []rune(cmd) infos := lexCmdline(raw) return &Args{raw: raw, infos: infos} } // Shortcut for LexArgs(cmd).Args() func SplitArgs(cmd string) []string { args := LexArgs(cmd) return args.Args() } // Build a shell command from multiple arguments. func QuoteArgs(args ...string) *Args { quoted := make([]string, len(args)) for i, arg := range args { quoted[i] = QuoteArg(arg) } return LexArgs(strings.Join(quoted, " ")) } // Wrap a single argument with appropriate quoting so that it can be used // in a shell command. func QuoteArg(arg string) string { if strings.ContainsAny(arg, " '\"|?&!#$;[](){}<>*\n\t") { // "foo bar" --> "'foo bar'" // "foo'bar" --> "'foo'"'"'bar'" arg = "'" + strings.ReplaceAll(arg, "'", `'"'"'`) + "'" } return arg } // Get the number of arguments after interpreting shell quotes. func (a *Args) Count() int { return len(a.infos) } func (a *Args) LeadingSpace() string { if len(a.infos) > 0 { first := &a.infos[0] if first.start < len(a.raw) { return string(a.raw[:first.start]) } } return "" } func (a *Args) TrailingSpace() string { if len(a.infos) > 0 { last := &a.infos[len(a.infos)-1] if last.end < len(a.raw) { return string(a.raw[last.end:]) } } return "" } var ErrArgIndex = errors.New("argument index out of bounds") // Remove n arguments from the beginning of the command line. // Same semantics as the `shift` built-in shell command. // Will fail if shifting an invalid number of arguments. func (a *Args) ShiftSafe(n int) ([]string, error) { var shifted []string switch { case n == 0: shifted = []string{} case n > 0 && n < len(a.infos): for i := 0; i < n; i++ { shifted = append(shifted, a.infos[i].unquoted) } a.infos = a.infos[n:] start := a.infos[0].start a.raw = a.raw[start:] for i := range a.infos { a.infos[i].start -= start a.infos[i].end -= start } case n == len(a.infos): for i := 0; i < n; i++ { shifted = append(shifted, a.infos[i].unquoted) } a.raw = nil a.infos = nil default: return nil, ErrArgIndex } return shifted, nil } // Same as ShiftSafe but cannot fail. func (a *Args) Shift(n int) []string { if n < 0 { n = 0 } else if n > len(a.infos) { n = len(a.infos) } shifted, _ := a.ShiftSafe(n) return shifted } // Remove n arguments from the end of the command line. // Will fail if cutting an invalid number of arguments. func (a *Args) CutSafe(n int) ([]string, error) { var cut []string switch { case n == 0: cut = []string{} case n > 0 && n < len(a.infos): for i := len(a.infos) - n; i < len(a.infos); i++ { cut = append(cut, a.infos[i].unquoted) } a.infos = a.infos[:len(a.infos)-n] a.raw = a.raw[:a.infos[len(a.infos)-1].end] case n == len(a.infos): for i := 0; i < n; i++ { cut = append(cut, a.infos[i].unquoted) } a.raw = nil a.infos = nil default: return nil, ErrArgIndex } return cut, nil } // Same as CutSafe but cannot fail. func (a *Args) Cut(n int) []string { if n < 0 { n = 0 } else if n > len(a.infos) { n = len(a.infos) } cut, _ := a.CutSafe(n) return cut } // Insert the specified prefix at the beginning of the command line. func (a *Args) Prepend(cmd string) { prefix := []rune(cmd) infos := lexCmdline(prefix) if len(infos) > 0 && infos[len(infos)-1].end == len(prefix) { // No trailing white space to the added command line. // Add one space manually. prefix = append(prefix, ' ') } for i := range a.infos { a.infos[i].start += len(prefix) a.infos[i].end += len(prefix) } a.raw = append(prefix, a.raw...) a.infos = append(infos, a.infos...) } // Extend the command line with more arguments. func (a *Args) Extend(cmd string) { suffix := []rune(cmd) infos := lexCmdline(suffix) if len(infos) > 0 && infos[0].start == 0 { // No leading white space to the added command line. // Add one space manually. a.raw = append(a.raw, ' ') } for i := range infos { infos[i].start += len(a.raw) infos[i].end += len(a.raw) } a.raw = append(a.raw, suffix...) a.infos = append(a.infos, infos...) } // Get the nth argument after interpreting shell quotes. func (a *Args) ArgSafe(n int) (string, error) { if n < 0 || n >= len(a.infos) { return "", ErrArgIndex } return a.infos[n].unquoted, nil } // Get the nth argument after interpreting shell quotes. // Will panic if the argument index does not exist. func (a *Args) Arg(n int) string { return a.infos[n].unquoted } // Get all arguments after interpreting shell quotes. func (a *Args) Args() []string { args := make([]string, 0, len(a.infos)) for i := range a.infos { args = append(args, a.infos[i].unquoted) } return args } // Get the raw command line, with uninterpreted shell quotes. func (a *Args) String() string { return string(a.raw) } // Make a deep copy of an Args object. func (a *Args) Clone() *Args { infos := make([]argInfo, len(a.infos)) copy(infos, a.infos) raw := make([]rune, len(a.raw)) copy(raw, a.raw) return &Args{raw: raw, infos: infos} } golang-sourcehut-rjarry-go-opt-2.0.1/args_test.go000066400000000000000000000122271474202223700220430ustar00rootroot00000000000000// SPDX-License-Identifier: MIT // Copyright (c) 2023 Robin Jarry package opt_test import ( "testing" "git.sr.ht/~rjarry/go-opt/v2" "github.com/stretchr/testify/assert" ) func TestLexArgs(t *testing.T) { vectors := []struct { cmd string args []string lead string trail string shift int shifted []string shiftArgs []string shiftString string shiftLead string shiftTrail string prepend string prependedArgs []string prependedString string prependedLead string prependedTrail string cut int cutted []string cutArgs []string cutString string cutLead string cutTrail string extend string extendedArgs []string extendedString string extendLead string extendTrail string }{ { cmd: "a b c", args: []string{"a", "b", "c"}, shift: 0, shifted: []string{}, shiftArgs: []string{"a", "b", "c"}, shiftString: "a b c", prepend: "z ", prependedArgs: []string{"z", "a", "b", "c"}, prependedString: "z a b c", cut: 0, cutted: []string{}, cutArgs: []string{"z", "a", "b", "c"}, cutString: "z a b c", extend: " x", extendedArgs: []string{"z", "a", "b", "c", "x"}, extendedString: "z a b c x", }, { cmd: " 'foo'\t-bar c $d | zz $bar 'x y z' ", args: []string{"foo", "-bar", "c", "$d", "|", "zz", "$bar", "x y z"}, lead: " ", trail: " ", shift: 2, shifted: []string{"foo", "-bar"}, shiftArgs: []string{"c", "$d", "|", "zz", "$bar", "x y z"}, shiftString: "c $d | zz $bar 'x y z' ", shiftTrail: " ", prepend: `baz -p "$aeiouy noooo"`, prependedArgs: []string{"baz", "-p", "$aeiouy noooo", "c", "$d", "|", "zz", "$bar", "x y z"}, prependedString: `baz -p "$aeiouy noooo" c $d | zz $bar 'x y z' `, prependedLead: "", prependedTrail: " ", cut: 1, cutted: []string{"x y z"}, cutArgs: []string{"baz", "-p", "$aeiouy noooo", "c", "$d", "|", "zz", "$bar"}, cutString: `baz -p "$aeiouy noooo" c $d | zz $bar`, extend: "'eeeee eeee ", extendedArgs: []string{"baz", "-p", "$aeiouy noooo", "c", "$d", "|", "zz", "$bar", "eeeee eeee "}, extendedString: `baz -p "$aeiouy noooo" c $d | zz $bar 'eeeee eeee `, extendTrail: "", }, { cmd: `foo -xz \"bar 'baz\"' "\$baz \" ok"`, args: []string{"foo", "-xz", `"bar`, `baz\"`, `$baz " ok`}, shift: 2, shifted: []string{"foo", "-xz"}, shiftArgs: []string{`"bar`, `baz\"`, `$baz " ok`}, shiftString: `\"bar 'baz\"' "\$baz \" ok"`, prepend: "find 'bleh' | xargs -uuuuu u", prependedArgs: []string{"find", "bleh", "|", "xargs", "-uuuuu", "u", `"bar`, `baz\"`, `$baz " ok`}, prependedString: `find 'bleh' | xargs -uuuuu u \"bar 'baz\"' "\$baz \" ok"`, cut: 2, cutted: []string{`baz\"`, `$baz " ok`}, cutArgs: []string{"find", "bleh", "|", "xargs", "-uuuuu", "u", `"bar`}, cutString: `find 'bleh' | xargs -uuuuu u \"bar`, extend: "|| rm -rf / &", extendedArgs: []string{"find", "bleh", "|", "xargs", "-uuuuu", "u", `"bar`, "||", "rm", "-rf", "/", "&"}, extendedString: `find 'bleh' | xargs -uuuuu u \"bar || rm -rf / &`, }, } for _, vec := range vectors { t.Run(vec.cmd, func(t *testing.T) { cmd := opt.LexArgs(vec.cmd) assert.Equal(t, vec.args, cmd.Args(), "args") assert.Equal(t, vec.lead, cmd.LeadingSpace(), "LeadingSpace") assert.Equal(t, vec.trail, cmd.TrailingSpace(), "TrailingSpace") shifted := cmd.Shift(vec.shift) assert.Equal(t, vec.shifted, shifted, "Shift") assert.Equal(t, vec.shiftArgs, cmd.Args(), "shifted.Args") assert.Equal(t, vec.shiftString, cmd.String(), "shifted.String") assert.Equal(t, vec.shiftLead, cmd.LeadingSpace(), "shifted.LeadingSpace") assert.Equal(t, vec.shiftTrail, cmd.TrailingSpace(), "shifted.TrailingSpace") cmd.Prepend(vec.prepend) assert.Equal(t, vec.prependedArgs, cmd.Args(), "prepended.Args") assert.Equal(t, vec.prependedString, cmd.String(), "prepended.String") assert.Equal(t, vec.prependedLead, cmd.LeadingSpace(), "prepended.LeadingSpace") assert.Equal(t, vec.prependedTrail, cmd.TrailingSpace(), "prepended.TrailingSpace") cutted := cmd.Cut(vec.cut) assert.Equal(t, vec.cutted, cutted, "Cut") assert.Equal(t, vec.cutArgs, cmd.Args(), "cut.Args") assert.Equal(t, vec.cutString, cmd.String(), "cut.String") assert.Equal(t, vec.cutLead, cmd.LeadingSpace(), "cut.LeadingSpace") assert.Equal(t, vec.cutTrail, cmd.TrailingSpace(), "cut.TrailingSpace") cmd.Extend(vec.extend) assert.Equal(t, vec.extendedArgs, cmd.Args(), "extend.Args") assert.Equal(t, vec.extendedString, cmd.String(), "extend.String") assert.Equal(t, vec.extendLead, cmd.LeadingSpace(), "extend.LeadingSpace") assert.Equal(t, vec.extendTrail, cmd.TrailingSpace(), "extend.TrailingSpace") }) } } golang-sourcehut-rjarry-go-opt-2.0.1/complete.go000066400000000000000000000065641474202223700216670ustar00rootroot00000000000000package opt import ( "reflect" "strings" ) type Completion struct { Value string Description string } func (c *CmdSpec) unseenFlags(arg string) []Completion { var flags []Completion for i := 0; i < len(c.opts); i++ { spec := &c.opts[i] if !spec.appliesToAlias(c.name) || spec.seen { continue } switch spec.kind { case flag, option: if spec.short != "" && strings.HasPrefix(spec.short, arg) { flags = append(flags, Completion{ Value: spec.short + " ", Description: spec.description, }) } if spec.long != "" && strings.HasPrefix(spec.long, arg) { flags = append(flags, Completion{ Value: spec.long + " ", Description: spec.description, }) } } } return flags } func (c *CmdSpec) nextPositional() *optSpec { var spec *optSpec for p := len(c.positionals) - 1; p >= 0; p-- { spec = &c.opts[c.positionals[p]] if !spec.appliesToAlias(c.name) { continue } if spec.seenValue && (p+1) < len(c.positionals) { // first "unseen" positional argument return &c.opts[c.positionals[p+1]] } } return spec } func (s *optSpec) getCompletions(arg string) []Completion { if s.complete.IsValid() { in := []reflect.Value{reflect.ValueOf(arg)} out := s.complete.Call(in) if res, ok := out[0].Interface().([]string); ok { var completions []Completion for _, value := range res { completions = append(completions, Completion{ Value: value, Description: s.description, }) } return completions } } return nil } func (c *CmdSpec) GetCompletions(args *Args) ([]Completion, string) { if args.Count() == 0 || (args.Count() == 1 && args.TrailingSpace() == "") { return nil, "" } var completions []Completion var prefix string var flags []Completion var last *seenArg var spec *optSpec _ = c.parseArgs(args.Clone()) if len(c.seen) > 0 { last = c.seen[len(c.seen)-1] } if args.TrailingSpace() != "" { // Complete new argument prefix = args.String() if last != nil && !last.spec.seenValue { spec = last.spec } if spec == nil { // Last argument was not a flag that required a value. // Complete for the next unseen positional argument. flags = c.unseenFlags("") spec = c.nextPositional() } if spec != nil { completions = spec.getCompletions("") } } else { // Complete current argument arg := args.Cut(1)[0] prefix = args.String() + " " if last != nil && last.indexes[len(last.indexes)-1] == args.Count() { s := last.spec f := s.long + "=" switch { case (s.kind == flag || s.kind == option) && (s.short == arg || s.long == arg): // Current argument is precisely a flag. spec = nil completions = []Completion{{Value: arg + " ", Description: s.description}} case s.kind == option && f != "=" && strings.HasPrefix(arg, f): // Current argument is a long flag in the format: // --flag=value // Strip the prefix and complete the value. prefix += f arg = strings.TrimPrefix(arg, f) fallthrough default: spec = s } } else { // Current argument was not identified, attempt // completion from the next unseen positional. spec = c.nextPositional() } if spec != nil { completions = spec.getCompletions(arg) } if strings.HasPrefix(arg, "-") { flags = c.unseenFlags(arg) } } if flags != nil { completions = append(completions, flags...) } return completions, prefix } golang-sourcehut-rjarry-go-opt-2.0.1/complete_test.go000066400000000000000000000105031474202223700227120ustar00rootroot00000000000000package opt_test import ( "strings" "testing" "git.sr.ht/~rjarry/go-opt/v2" "github.com/stretchr/testify/assert" ) type CompleteStruct struct { Name string `opt:"-n,--name" required:"true" complete:"CompleteName" desc:"Ninja turtle name."` Delay float64 `opt:"--delay"` Zero bool `opt:"-z" description:"Print zero values"` Backoff bool `opt:"-B,--backoff" desc:"Increase delay on error"` Tags []string `opt:"..." complete:"CompleteTag"` } func (c *CompleteStruct) CompleteName(arg string) []string { var names []string for _, n := range []string{"leonardo", "michelangelo", "rafaelo", "donatello"} { if strings.HasPrefix(n, arg) { names = append(names, n) } } return names } func (c *CompleteStruct) CompleteTag(arg string) []string { var results []string prefix := "" if strings.HasPrefix(arg, "-") { prefix = "-" } else if strings.HasPrefix(arg, "+") { prefix = "+" } tags := []string{"unread", "sent", "important", "inbox", "trash"} for _, t := range tags { t = prefix + t if strings.HasPrefix(t, arg) { results = append(results, t) } } return results } func TestComplete(t *testing.T) { vectors := []struct { cmdline string completions []opt.Completion prefix string }{ { "foo --delay 33..33.3 -n", []opt.Completion{{Value: "-n ", Description: "Ninja turtle name."}}, "foo --delay 33..33.3 ", }, { "foo --delay 33..33.3 -n ", []opt.Completion{ {Value: "leonardo", Description: "Ninja turtle name."}, {Value: "michelangelo", Description: "Ninja turtle name."}, {Value: "rafaelo", Description: "Ninja turtle name."}, {Value: "donatello", Description: "Ninja turtle name."}, }, "foo --delay 33..33.3 -n ", }, { "foo --delay 33..33.3 -n don", []opt.Completion{{Value: "donatello", Description: "Ninja turtle name."}}, "foo --delay 33..33.3 -n ", }, { "foo --delay 33..33.3 --name=", []opt.Completion{ {Value: "leonardo", Description: "Ninja turtle name."}, {Value: "michelangelo", Description: "Ninja turtle name."}, {Value: "rafaelo", Description: "Ninja turtle name."}, {Value: "donatello", Description: "Ninja turtle name."}, }, "foo --delay 33..33.3 --name=", }, { "foo --delay 33..33.3 --name=leo", []opt.Completion{{Value: "leonardo", Description: "Ninja turtle name."}}, "foo --delay 33..33.3 --name=", }, { "foo --nam", []opt.Completion{{Value: "--name ", Description: "Ninja turtle name."}}, "foo ", }, { "foo --delay 33..33.3 --backoff", []opt.Completion{{Value: "--backoff ", Description: "Increase delay on error"}}, "foo --delay 33..33.3 ", }, { "foo --delay 33..33.3 -", []opt.Completion{ {Value: "-unread"}, {Value: "-sent"}, {Value: "-important"}, {Value: "-inbox"}, {Value: "-trash"}, {Value: "-n ", Description: "Ninja turtle name."}, {Value: "--name ", Description: "Ninja turtle name."}, {Value: "-z ", Description: "Print zero values"}, {Value: "-B ", Description: "Increase delay on error"}, {Value: "--backoff ", Description: "Increase delay on error"}, }, "foo --delay 33..33.3 ", }, { "foo --delay 33..33.3 ", []opt.Completion{ {Value: "unread"}, {Value: "sent"}, {Value: "important"}, {Value: "inbox"}, {Value: "trash"}, {Value: "-n ", Description: "Ninja turtle name."}, {Value: "--name ", Description: "Ninja turtle name."}, {Value: "-z ", Description: "Print zero values"}, {Value: "-B ", Description: "Increase delay on error"}, {Value: "--backoff ", Description: "Increase delay on error"}, }, "foo --delay 33..33.3 ", }, { "foo --delay 33..33.3 -n leonardo i", []opt.Completion{{Value: "important"}, {Value: "inbox"}}, "foo --delay 33..33.3 -n leonardo ", }, { "foo +", []opt.Completion{ {Value: "+unread"}, {Value: "+sent"}, {Value: "+important"}, {Value: "+inbox"}, {Value: "+trash"}, }, "foo ", }, { "foo -i", []opt.Completion{{Value: "-important"}, {Value: "-inbox"}}, "foo ", }, } for _, v := range vectors { t.Run(v.cmdline, func(t *testing.T) { args := opt.LexArgs(v.cmdline) spec := opt.NewCmdSpec(args.Arg(0), new(CompleteStruct)) completions, prefix := spec.GetCompletions(args) assert.Equal(t, v.completions, completions) assert.Equal(t, v.prefix, prefix) }) } } golang-sourcehut-rjarry-go-opt-2.0.1/go.mod000066400000000000000000000003531474202223700206240ustar00rootroot00000000000000module git.sr.ht/~rjarry/go-opt/v2 go 1.18 require github.com/stretchr/testify v1.8.4 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-sourcehut-rjarry-go-opt-2.0.1/go.sum000066400000000000000000000015611474202223700206530ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang-sourcehut-rjarry-go-opt-2.0.1/opt.go000066400000000000000000000105541474202223700206530ustar00rootroot00000000000000package opt /* Example use: ```go package main import ( "fmt" "log" "git.sr.ht/~rjarry/go-opt/v2" ) type Foo struct { Delay time.Duration `opt:"-t" action:"ParseDelay" default:"1s"` Force bool `opt:"-f"` Name string `opt:"name" required:"false" metavar:"FOO"` Cmd []string `opt:"..."` } func (f *Foo) ParseDelay(arg string) error { d, err := time.ParseDuration(arg) if err != nil { return err } f.Delay = d return nil } func main() { var foo Foo err := opt.CmdlineToStruct("foo -f bar baz 'xy z' ", &foo) if err != nil { log.Fatalf("error: %s\n", err) } fmt.Printf("%#v\n", foo) } ``` ```console $ foo -f bar baz 'xy z' Foo{Delay: 1000000000, Force: true, Name: "bar", Cmd: []string{"baz", "xy z"}} $ foo -t error: -t takes a value. Usage: foo [-t ] [-f] [] ... ``` ## Supported tags There is a set of tags that can be set on struct fields: ### `opt:"-f"` Registers that this field is associated to the specified flag. Unless a custom `action` method is specified, the flag value will be automatically converted from string to the field type (only basic scalar types are supported: all integers (both signed and unsigned), all floats and strings. If the field type is `bool`, the flag will take no value. Only supports short flags for now. ### `opt:"blah"` Field is associated to a positional argument. ### `opt:"..."` Field will be mapped to all remaining arguments. If the field is `string`, the raw command line will be stored preserving any shell quoting, otherwise the field needs to be `[]string` and will receive the remaining arguments after interpreting shell quotes. ### `opt:"-"` Special value to indicate that this command will accept any argument without any check nor parsing. The field on which it is set will not be updated and should be called `Unused struct{}` as a convention. The field name must start with an upper case to avoid linter warnings because of unused fields. ### `action:"ParseFoo"` Custom method to be used instead of the default automatic conversion. Needs to be a method with a pointer receiver to the struct itself, takes a single `string` argument and may return an `error` to abort parsing. The `action` method is responsible of updating the struct. ### `description:"foobaz"` or `desc:"foobaz"` A description that is returned alongside arguments during autocompletion. ### `default:"foobaz"` Default `string` value if not specified by the user. Will be processed by the same conversion/parsing as any other argument. ### `metavar:"foo|bar|baz"` Displayed name for argument values in the generated usage help. ### `required:"true|false"` By default, flag arguments are optional and positional arguments are required. Using this tag allows changing that default behaviour. If an argument is not required, it will be surrounded by square brackets in the generated usage help. ### `aliases:"cmd1,cmd2"` By default, arguments are interpreted for all command aliases. If this is specified, this field/option will only be applicable to the specified command aliases. ### `complete:"CompleteFoo"` Custom method to return the valid completions for the annotated option / argument. Needs to be a method with a pointer receiver to the struct itself, takes a single `string` argument and must return a `[]string` slice containing the valid completions. ## Caveats Depending on field types, the argument string values are parsed using the appropriate conversion function. If no `opt` tag is set on a field, it will be excluded from automated argument parsing. It can still be updated indirectly via a custom `action` method. Short flags can be combined like with `getopt(3)`: * Flags with no value: `-abc` is equivalent to `-a -b -c` * Flags with a value (options): `-j9` is equivalent to `-j 9` */ import ( "errors" "fmt" ) func CmdlineToStruct(cmdline string, v any) error { args := LexArgs(cmdline) return ArgsToStruct(args, v) } func ArgsToStruct(args *Args, v any) error { if args.Count() == 0 { return errors.New("empty command") } cmd := NewCmdSpec(args.Arg(0), v) if err := cmd.ParseArgs(args); err != nil { return fmt.Errorf("%w. Usage: %s", err, cmd.Usage()) } return nil } func GetCompletions(cmdline string, v any) (completions []Completion, prefix string) { args := LexArgs(cmdline) if args.Count() == 0 { return nil, "" } spec := NewCmdSpec(args.Arg(0), v) return spec.GetCompletions(args) } golang-sourcehut-rjarry-go-opt-2.0.1/shlex.go000066400000000000000000000074261474202223700212000ustar00rootroot00000000000000// This code has been inspired from https://github.com/google/shlex // // Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // The following changes are published under the MIT license: // // * Internal variables renamed // * Recording of the original argument start position in the string // * Reformatting of some code parts. // // SPDX-License-Identifier: MIT // Copyright (c) 2023 Robin Jarry package opt // runeClass is the type of a UTF-8 character classification: A quote, space, // escape. type runeClass int const ( other runeClass = iota space doubleQuote singleQuote backslash ) var runeClasses = map[rune]runeClass{ ' ': space, '\t': space, '\r': space, '\n': space, '"': doubleQuote, '\'': singleQuote, '\\': backslash, } // the internal state used by the lexer state machine type lexerState int // Lexer state machine states const ( // no runes have been seen start lexerState = iota // processing regular runes in a word inWord // we have just consumed an escape rune; the next rune is literal escaping // we have just consumed an escape rune within a quoted string escapingQuoted // we are within a quoted string that supports escaping ("...") inDoubleQuote // we are within a string that does not support escaping ('...') inSingleQuote ) // Each argument info contains the start offset of the raw argument in the // command line (including shell escapes, quotes, etc.), and its "unquoted" // value after interpreting shell quotes and escapes. type argInfo struct { start int end int unquoted string } // Parse a raw command line and return a list of argument info structs func lexCmdline(raw []rune) []argInfo { var state lexerState var unquoted []rune var argstart int var infos []argInfo state = start for i, char := range raw { class := runeClasses[char] switch state { case start: // no runes read yet switch class { case space: break case doubleQuote: state = inDoubleQuote case singleQuote: state = inSingleQuote case backslash: state = escaping default: // start a new word unquoted = []rune{char} state = inWord } argstart = i case inWord: switch class { case space: infos = append(infos, argInfo{ start: argstart, end: i, unquoted: string(unquoted), }) unquoted = nil state = start case doubleQuote: state = inDoubleQuote case singleQuote: state = inSingleQuote case backslash: state = escaping default: unquoted = append(unquoted, char) } case escaping: // the rune after an escape character state = inWord unquoted = append(unquoted, char) case escapingQuoted: // the next rune after an escape character, in double quotes state = inDoubleQuote unquoted = append(unquoted, char) case inDoubleQuote: switch class { case doubleQuote: state = inWord case backslash: state = escapingQuoted default: unquoted = append(unquoted, char) } case inSingleQuote: switch class { case singleQuote: state = inWord default: unquoted = append(unquoted, char) } } } if unquoted != nil { infos = append(infos, argInfo{ start: argstart, end: len(raw), unquoted: string(unquoted), }) } return infos } golang-sourcehut-rjarry-go-opt-2.0.1/spec.go000066400000000000000000000341071474202223700210030ustar00rootroot00000000000000package opt import ( "errors" "fmt" "reflect" "regexp" "strconv" "strings" ) // Command line options specifier type CmdSpec struct { // first argument, for Usage() generation name string // if true, all arguments will be ignored passthrough bool // list of option specs extracted from struct tags opts []optSpec // list of option specs in the order in which they were seen seen []*seenArg // indexes of short options in the list for quick access shortOpts map[string]int // indexes of long options in the list for quick access longOpts map[string]int // indexes of positional arguments positionals []int } type seenArg struct { spec *optSpec indexes []int } type optKind int const ( unset optKind = iota // dummy value to cause the whole command to be passthrough passthrough // flag without a value: "-f" or "--foo-baz" flag // flag with a value: "-f bar", "--foo-baz bar" or "--foo-baz=bar" option // positional argument after interpreting shell quotes positional // remaining positional arguments after interpreting shell quotes remainderSplit // remaining positional arguments without interpreting shell quotes remainder ) // Option or argument specifier type optSpec struct { // kind of option/argument kind optKind // argument is required required bool // option/argument description description string // argument was seen on the command line seen bool // argument value was seen on the command line (only applies to options) seenValue bool // "f", "foo-baz" (only when kind is flag or option) short, long string // name of option/argument value in usage help metavar string // default string value before interpretation defval string // only applies to the specified command aliases aliases []string // custom action method action reflect.Value // custom complete method complete reflect.Value // destination struct field dest reflect.Value } var ( shortOptRe = regexp.MustCompile(`^(-[a-zA-Z0-9])$`) longOptRe = regexp.MustCompile(`^(--[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9])$`) positionalRe = regexp.MustCompile(`^([a-zA-Z][\w-]*)$`) ) const optionDelim = "--" // Interpret all struct fields to a list of option specs func NewCmdSpec(name string, v any) *CmdSpec { typ := reflect.TypeOf(v) val := reflect.ValueOf(v) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } else { panic("NewCmdSpec requires a pointer") } if typ.Kind() != reflect.Struct { panic("NewCmdSpec requires a pointer to a struct") } cmd := &CmdSpec{ name: name, opts: make([]optSpec, 0, typ.NumField()), shortOpts: make(map[string]int), longOpts: make(map[string]int), } allPositionals := false for i := 0; i < typ.NumField(); i++ { var spec optSpec spec.parseField(reflect.ValueOf(v), typ.Field(i)) switch spec.kind { case unset: // ignored field continue case passthrough: cmd.passthrough = true continue } spec.dest = val.Field(i) if allPositionals { panic(`opt:"..." must be last`) } switch spec.kind { case flag, option: if spec.short != "" { cmd.shortOpts[spec.short] = len(cmd.opts) } if spec.long != "" { cmd.longOpts[spec.long] = len(cmd.opts) } case remainder, remainderSplit: allPositionals = true fallthrough default: cmd.positionals = append(cmd.positionals, len(cmd.opts)) } cmd.opts = append(cmd.opts, spec) } return cmd } func (spec *optSpec) parseField(struc reflect.Value, t reflect.StructField) { abort := func(msg string, args ...any) { msg = fmt.Sprintf(msg, args...) panic(fmt.Sprintf("%s.%s: %s", struc.Type(), t.Name, msg)) } // check what kind of argument this field maps to opt := t.Tag.Get("opt") switch { case opt == "-": // ignore all arguments (passthrough) for this command spec.kind = passthrough fallthrough case opt == "": // ignored field return case opt == "...": // remainder switch t.Type.Kind() { case reflect.Slice: if t.Type.Elem().Kind() != reflect.String { abort("'...' only works with []string") } spec.kind = remainderSplit case reflect.String: spec.kind = remainder default: abort("'...' only works with string or []string") } spec.metavar = "<" + strings.ToLower(t.Name) + ">..." spec.required = true case strings.Contains(opt, "-"): // flag or option for _, flag := range strings.Split(opt, ",") { m := longOptRe.FindStringSubmatch(flag) if m != nil { spec.long = m[1] continue } m = shortOptRe.FindStringSubmatch(flag) if m != nil { spec.short = m[1] continue } abort("invalid opt tag: %q", opt) } if t.Type.Kind() == reflect.Bool { spec.kind = flag } else { spec.kind = option spec.metavar = "<" + strings.ToLower(t.Name) + ">" } if spec.short == "" && spec.long == "" { abort("invalid opt tag: %q", opt) } case positionalRe.MatchString(opt): // named positional spec.kind = positional spec.metavar = "<" + strings.ToLower(opt) + ">" spec.required = true default: abort("invalid opt tag: %q", opt) } if metavar, hasMetavar := t.Tag.Lookup("metavar"); hasMetavar { // explicit metavar for the generated usage spec.metavar = metavar } spec.description = t.Tag.Get("description") if spec.description == "" { spec.description = t.Tag.Get("desc") } spec.defval = t.Tag.Get("default") switch t.Tag.Get("required") { case "true": spec.required = true case "false": spec.required = false case "": if spec.defval != "" { spec.required = false } default: abort("invalid required value") } if aliases := t.Tag.Get("aliases"); aliases != "" { spec.aliases = strings.Split(aliases, ",") } if methodName, found := t.Tag.Lookup("action"); found { method := struc.MethodByName(methodName) if !method.IsValid() { abort("action method not found: (*%s).%s", struc, methodName) } ok := method.Type().NumIn() == 1 ok = ok && method.Type().In(0).Kind() == reflect.String ok = ok && method.Type().NumOut() == 1 ok = ok && method.Type().Out(0).Kind() == reflect.Interface ok = ok && method.Type().Out(0).Name() == "error" if !ok { abort("(*%s).%s: invalid signature, expected func(string) error", struc.Elem().Type().Name(), methodName, t.Type.Kind()) } spec.action = method } if !spec.action.IsValid() { switch t.Type.Kind() { case reflect.String: fallthrough case reflect.Bool: fallthrough case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: fallthrough case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: fallthrough case reflect.Float32, reflect.Float64: fallthrough case reflect.Slice: break default: abort("unsupported field type: %s", t.Type.Kind()) } } if methodName, found := t.Tag.Lookup("complete"); found { method := struc.MethodByName(methodName) if !method.IsValid() { abort("complete method not found: (*%s).%s", struc, methodName) } if !(method.Type().NumIn() == 1 && method.Type().In(0).Kind() == reflect.String && method.Type().NumOut() == 1 && method.Type().Out(0).Kind() == reflect.Slice && method.Type().Out(0).Elem().Kind() == reflect.String) { abort("(*%s).%s: invalid signature, expected func(string) []string", struc.Elem().Type().Name(), methodName, t.Type.Kind()) } spec.complete = method } } func (s *optSpec) Usage() string { var usage string switch s.kind { case flag: if s.short != "" { usage = s.short } else { usage = s.long } case option: if s.short != "" { usage = s.short } else { usage = s.long } usage += " " + s.metavar default: usage = s.metavar } if s.required { return usage } return "[" + usage + "]" } func (s *optSpec) Flag() string { var usage string switch s.kind { case flag, option: if s.short != "" { usage = s.short } else { usage = s.long } default: usage = s.metavar } return usage } func (c *CmdSpec) Usage() string { if c.passthrough { return c.name + " ..." } args := []string{c.name} for _, spec := range c.opts { if spec.appliesToAlias(c.name) { args = append(args, spec.Usage()) } } return strings.Join(args, " ") } type errKind int const ( unknownErr errKind = iota missingValue takesNoValue unknownFlag unexpectedArg badValue requiredArg ) type ArgError struct { kind errKind spec *optSpec err error } func (e *ArgError) Error() string { switch e.kind { case missingValue: return fmt.Sprintf("%s takes a value", e.spec.Flag()) case takesNoValue: return fmt.Sprintf("%s does not take a value", e.spec.Flag()) case unknownFlag: return fmt.Sprintf("%s unknown flag", e.err) case unexpectedArg: return fmt.Sprintf("%q unexpected argument", e.err.Error()) case badValue: return fmt.Sprintf("%s: %s", e.spec.Flag(), e.err) case requiredArg: return fmt.Sprintf("%s is required", e.spec.Flag()) default: return fmt.Sprintf("unknown error: %#v", e) } } func (c *CmdSpec) ParseArgs(args *Args) error { if c.passthrough { return nil } if errors := c.parseArgs(args); len(errors) > 0 { return errors[0] } return nil } func (c *CmdSpec) markSeen(spec *optSpec, index int) *seenArg { spec.seen = true arg := &seenArg{spec: spec, indexes: []int{index}} c.seen = append(c.seen, arg) return arg } func (c *CmdSpec) parseArgs(args *Args) []*ArgError { var cur *seenArg var argErrors []*ArgError fail := func(kind errKind, s *optSpec, detail error) { argErrors = append(argErrors, &ArgError{kind, s, detail}) } positionals := c.positionals ignoreFlags := false c.seen = nil args.Shift(1) // skip command name i := 1 for args.Count() > 0 { arg := args.Arg(0) switch { case c.getLongFlag(arg) != nil && !ignoreFlags: if cur != nil { fail(missingValue, cur.spec, nil) } arg, val, hasValue := strings.Cut(arg, "=") cur = c.markSeen(&c.opts[c.longOpts[arg]], i) if cur.spec.kind == flag { if hasValue { fail(takesNoValue, cur.spec, nil) cur = nil goto next } if err := cur.spec.parseValue("true"); err != nil { fail(badValue, cur.spec, err) } cur = nil } else if hasValue { if err := cur.spec.parseValue(val); err != nil { fail(badValue, cur.spec, err) } cur = nil } case c.getShortFlag(arg) != nil && !ignoreFlags: if cur != nil { fail(missingValue, cur.spec, nil) } arg = arg[1:] for len(arg) > 0 { f := arg[:1] arg = arg[1:] var spec *optSpec if o, ok := c.shortOpts["-"+f]; ok { spec = &c.opts[o] } if spec == nil || !spec.appliesToAlias(c.name) { fail(unknownFlag, spec, errors.New(f)) goto next } cur = c.markSeen(spec, i) if spec.kind == flag { if err := spec.parseValue("true"); err != nil { fail(badValue, spec, err) } cur = nil } else if len(arg) > 0 { if err := spec.parseValue(arg); err != nil { fail(badValue, spec, err) } cur = nil arg = "" } } default: // positional if cur != nil { // separate value for a short/long flag cur.indexes = append(cur.indexes, i) if err := cur.spec.parseValue(arg); err != nil { fail(badValue, cur.spec, err) } cur = nil goto next } for len(positionals) > 0 { spec := &c.opts[positionals[0]] positionals = positionals[1:] if spec.appliesToAlias(c.name) { cur = c.markSeen(spec, i) break } } if arg == optionDelim { ignoreFlags = true goto next } if cur == nil { fail(unexpectedArg, nil, errors.New(arg)) goto next } switch cur.spec.kind { case remainder: cur.spec.dest.SetString(args.String()) for args.Count() > 0 { i += 1 cur.indexes = append(cur.indexes, i) args.Shift(1) } cur.spec.seenValue = true case remainderSplit: cur.spec.dest.Set(reflect.ValueOf(args.Args())) for args.Count() > 0 { i += 1 cur.indexes = append(cur.indexes, i) args.Shift(1) } cur.spec.seenValue = true default: if err := cur.spec.parseValue(arg); err != nil { fail(badValue, cur.spec, err) } } cur = nil } next: args.Shift(1) i += 1 } if cur != nil { fail(missingValue, cur.spec, nil) } for i := 0; i < len(c.opts); i++ { spec := &c.opts[i] if !spec.appliesToAlias(c.name) { continue } if !spec.seen && spec.defval != "" { if err := spec.parseValue(spec.defval); err != nil { fail(missingValue, spec, nil) } } else if spec.required && !spec.seen { fail(requiredArg, spec, nil) } } return argErrors } func (c *CmdSpec) getShortFlag(arg string) *optSpec { if len(arg) <= 1 || !strings.HasPrefix(arg, "-") { return nil } if o, ok := c.shortOpts[arg[:2]]; ok { spec := &c.opts[o] if spec.appliesToAlias(c.name) { return spec } } return nil } func (c *CmdSpec) getLongFlag(arg string) *optSpec { if len(arg) <= 2 || !strings.HasPrefix(arg, "--") { return nil } arg, _, _ = strings.Cut(arg, "=") if o, ok := c.longOpts[arg]; ok { spec := &c.opts[o] if spec.appliesToAlias(c.name) { return spec } } return nil } func (s *optSpec) parseValue(arg string) error { s.seenValue = true if s.action.IsValid() { in := []reflect.Value{reflect.ValueOf(arg)} out := s.action.Call(in) err, _ := out[0].Interface().(error) return err } switch s.dest.Type().Kind() { case reflect.String: s.dest.SetString(arg) case reflect.Bool: if b, err := strconv.ParseBool(arg); err == nil { s.dest.SetBool(b) } else { return err } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if i, err := strconv.ParseInt(arg, 10, 64); err == nil { s.dest.SetInt(i) } else { return err } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if u, err := strconv.ParseUint(arg, 10, 64); err == nil { s.dest.SetUint(u) } else { return err } case reflect.Float32, reflect.Float64: if f, err := strconv.ParseFloat(arg, 64); err == nil { s.dest.SetFloat(f) } else { return err } default: return fmt.Errorf("unsupported type: %s", s.dest) } return nil } func (s *optSpec) appliesToAlias(alias string) bool { if s.aliases == nil { return true } for _, a := range s.aliases { if a == alias { return true } } return false } golang-sourcehut-rjarry-go-opt-2.0.1/spec_test.go000066400000000000000000000046771474202223700220530ustar00rootroot00000000000000package opt_test import ( "fmt" "testing" "git.sr.ht/~rjarry/go-opt/v2" "github.com/stretchr/testify/assert" ) type OptionStruct struct { Jobs int `opt:"-j,--jobs" required:"true"` Delay float64 `opt:"--delay" default:"0.5"` Zero bool `opt:"-z" aliases:"baz"` Backoff bool `opt:"-B,--backoff"` Name string `opt:"name" aliases:"bar" action:"ParseName"` } func (o *OptionStruct) ParseName(arg string) error { if arg == "invalid" { return fmt.Errorf("%q invalid value", arg) } o.Name = arg return nil } func TestArgsToStructErrors(t *testing.T) { vectors := []struct { cmdline string err string }{ {"foo", "-j is required"}, {"foo -j", "-j takes a value"}, {"foo --delay -B", "--delay takes a value"}, {"bar -j4", " is required"}, {"foo -j f", `strconv.ParseInt: parsing "f": invalid syntax.`}, {"foo --delay=m", `strconv.ParseFloat: parsing "m": invalid syntax.`}, {"foo --jobs 8 baz", `"baz" unexpected argument`}, {"foo -u8 hop", `"-u8" unexpected argument`}, {"foo -z", `"-z" unexpected argument`}, {"bar -j4 foo baz", `"baz" unexpected argument`}, {"bar -j4 invalid", `invalid value`}, } for _, v := range vectors { t.Run(v.cmdline, func(t *testing.T) { err := opt.CmdlineToStruct(v.cmdline, new(OptionStruct)) assert.ErrorContains(t, err, v.err) }) } spec := opt.NewCmdSpec("bar", new(OptionStruct)) assert.Equal(t, spec.Usage(), "bar -j [--delay ] [-B] ") } func TestArgsToStruct(t *testing.T) { vectors := []struct { cmdline string expected OptionStruct }{ { cmdline: `bar -j4 'f o o \(°