diff --git a/.gitignore b/.gitignore index 49ad16c..cdfdacb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ _testmain.go # coverage droppings profile.cov +/*.test diff --git a/README.md b/README.md index d03f8da..d38457c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ docopt-go ========= -[![Build Status](https://travis-ci.org/docopt/docopt.go.svg?branch=master)](https://travis-ci.org/docopt/docopt.go) -[![Coverage Status](https://coveralls.io/repos/github/docopt/docopt.go/badge.svg)](https://coveralls.io/github/docopt/docopt.go) -[![GoDoc](https://godoc.org/github.com/docopt/docopt.go?status.svg)](https://godoc.org/github.com/docopt/docopt.go) +[![Build Status](https://travis-ci.org/kovetskiy/docopt-go.svg?branch=master)](https://travis-ci.org/kovetskiy/docopt-go) +[![Coverage Status](https://coveralls.io/repos/github/kovetskiy/docopt-go/badge.svg)](https://coveralls.io/github/kovetskiy/docopt-go) +[![GoDoc](https://godoc.org/github.com/kovetskiy/docopt-go?status.svg)](https://godoc.org/github.com/kovetskiy/docopt-go) An implementation of [docopt](http://docopt.org/) in the [Go](http://golang.org/) programming language. @@ -14,7 +14,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { @@ -47,13 +47,13 @@ Options: ⚠ Use the alias "docopt-go". To use docopt in your Go code: ```go -import "github.com/docopt/docopt-go" +import "github.com/kovetskiy/docopt-go" ``` To install docopt in your `$GOPATH`: ```console -$ go get github.com/docopt/docopt-go +$ go get github.com/kovetskiy/docopt-go ``` ## API @@ -103,7 +103,7 @@ var config struct { opts.Bind(&config) ``` -More documentation is available at [godoc.org](https://godoc.org/github.com/docopt/docopt-go). +More documentation is available at [godoc.org](https://godoc.org/github.com/kovetskiy/docopt-go). ## Unit Testing @@ -111,6 +111,6 @@ Unit testing your own usage docs is recommended, so you can be sure that for a g ## Tests -All tests from the Python version are implemented and passing at [Travis CI](https://travis-ci.org/docopt/docopt-go). New language-agnostic tests have been added to [test_golang.docopt](test_golang.docopt). +All tests from the Python version are implemented and passing at [Travis CI](https://travis-ci.org/kovetskiy/docopt-go). New language-agnostic tests have been added to [test_golang.docopt](test_golang.docopt). To run tests for docopt-go, use `go test`. diff --git a/example_test.go b/example_test.go index 4770824..ad145a1 100644 --- a/example_test.go +++ b/example_test.go @@ -2,6 +2,7 @@ package docopt import ( "fmt" + "net" "sort" ) @@ -40,10 +41,17 @@ func ExampleParseArgs() { } func ExampleOpts_Bind() { - usage := `Usage: - example tcp [...] [--force] [--timeout=] + usage := `docopt-go example + +Usage: + example tcp [...] [--force] [--timeout=] [--gateway=] example serial [--baud=] [--timeout=] - example --help | --version` + example --help | --version + +Options: + --gateway Set the gateway IP address [default: 192.168.0.1] + -h --help Show this screen. +` // Parse the command line `example serial 443 --baud=9600` argv := []string{"serial", "443", "--baud=9600"} @@ -57,13 +65,19 @@ func ExampleOpts_Bind() { Force bool Timeout int Baud int + Gateway net.IP } opts.Bind(&conf) if conf.Serial { - fmt.Printf("port: %d, baud: %d", conf.Port, conf.Baud) + fmt.Printf( + "port: %d, baud: %d, gateway: %v", + conf.Port, + conf.Baud, + conf.Gateway, + ) } // Output: - // port: 443, baud: 9600 + // port: 443, baud: 9600, gateway: 192.168.0.1 } diff --git a/examples/arguments/arguments.go b/examples/arguments/arguments.go index 10074cb..727a8b1 100644 --- a/examples/arguments/arguments.go +++ b/examples/arguments/arguments.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) var usage = `Usage: arguments [-vqrh] [FILE] ... diff --git a/examples/arguments/arguments_test.go b/examples/arguments/arguments_test.go index e9a8654..dcc4ad2 100644 --- a/examples/arguments/arguments_test.go +++ b/examples/arguments/arguments_test.go @@ -1,7 +1,7 @@ package main import ( - "github.com/docopt/docopt-go/examples" + "github.com/kovetskiy/docopt-go/examples" ) func Example() { diff --git a/examples/calculator/calculator.go b/examples/calculator/calculator.go index 16939dc..6bdcf0c 100644 --- a/examples/calculator/calculator.go +++ b/examples/calculator/calculator.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) var usage = `Not a serious example. diff --git a/examples/calculator/calculator_test.go b/examples/calculator/calculator_test.go index eee1c13..089579f 100644 --- a/examples/calculator/calculator_test.go +++ b/examples/calculator/calculator_test.go @@ -1,7 +1,7 @@ package main import ( - "github.com/docopt/docopt-go/examples" + "github.com/kovetskiy/docopt-go/examples" ) func Example() { diff --git a/examples/config_file/config_file.go b/examples/config_file/config_file.go index bfa174c..9c5c7fb 100644 --- a/examples/config_file/config_file.go +++ b/examples/config_file/config_file.go @@ -3,7 +3,7 @@ package main import ( "encoding/json" "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" "strings" ) diff --git a/examples/counted/counted.go b/examples/counted/counted.go index c5d0c33..d42994d 100644 --- a/examples/counted/counted.go +++ b/examples/counted/counted.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) var usage = `Usage: counted --help diff --git a/examples/counted/counted_test.go b/examples/counted/counted_test.go index 61f7cbc..faba1e1 100644 --- a/examples/counted/counted_test.go +++ b/examples/counted/counted_test.go @@ -1,7 +1,7 @@ package main import ( - "github.com/docopt/docopt-go/examples" + "github.com/kovetskiy/docopt-go/examples" ) func Example() { diff --git a/examples/examples.go b/examples/examples.go index 180d79b..86517a2 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -5,7 +5,7 @@ import ( "sort" "strings" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) // TestUsage is a helper used to test the output from the examples in this folder. diff --git a/examples/fake-git/branch/git_branch.go b/examples/fake-git/branch/git_branch.go index b77beee..d3d153c 100644 --- a/examples/fake-git/branch/git_branch.go +++ b/examples/fake-git/branch/git_branch.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/fake-git/checkout/git_checkout.go b/examples/fake-git/checkout/git_checkout.go index 0b9235c..b5b4cec 100644 --- a/examples/fake-git/checkout/git_checkout.go +++ b/examples/fake-git/checkout/git_checkout.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/fake-git/clone/git_clone.go b/examples/fake-git/clone/git_clone.go index 92adfcb..9a782f9 100644 --- a/examples/fake-git/clone/git_clone.go +++ b/examples/fake-git/clone/git_clone.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/fake-git/fakegit.go b/examples/fake-git/fakegit.go index 92e1ab5..ed0dc36 100644 --- a/examples/fake-git/fakegit.go +++ b/examples/fake-git/fakegit.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" "os" "os/exec" ) diff --git a/examples/fake-git/push/git_push.go b/examples/fake-git/push/git_push.go index 2b47edc..622667b 100644 --- a/examples/fake-git/push/git_push.go +++ b/examples/fake-git/push/git_push.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/fake-git/remote/git_remote.go b/examples/fake-git/remote/git_remote.go index c1d31e1..b86b8de 100644 --- a/examples/fake-git/remote/git_remote.go +++ b/examples/fake-git/remote/git_remote.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/naval_fate/naval_fate.go b/examples/naval_fate/naval_fate.go index 4827d44..46a88e1 100644 --- a/examples/naval_fate/naval_fate.go +++ b/examples/naval_fate/naval_fate.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/odd_even/odd_even.go b/examples/odd_even/odd_even.go index 1d9e617..115fe64 100644 --- a/examples/odd_even/odd_even.go +++ b/examples/odd_even/odd_even.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/options/options.go b/examples/options/options.go index b3c3398..6a2afba 100644 --- a/examples/options/options.go +++ b/examples/options/options.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/options_shortcut/options_shortcut.go b/examples/options_shortcut/options_shortcut.go index 6dbd394..1c4094c 100644 --- a/examples/options_shortcut/options_shortcut.go +++ b/examples/options_shortcut/options_shortcut.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/quick/quick.go b/examples/quick/quick.go index 63835a1..7c97fb0 100644 --- a/examples/quick/quick.go +++ b/examples/quick/quick.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/type_assert/type_assert.go b/examples/type_assert/type_assert.go index 61c2c4e..397d64a 100644 --- a/examples/type_assert/type_assert.go +++ b/examples/type_assert/type_assert.go @@ -2,7 +2,7 @@ package main import ( "fmt" - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" ) func main() { diff --git a/examples/unit_test/unit_test.go b/examples/unit_test/unit_test.go index 18428d9..79e58dc 100644 --- a/examples/unit_test/unit_test.go +++ b/examples/unit_test/unit_test.go @@ -1,7 +1,7 @@ package main import ( - "github.com/docopt/docopt-go" + "github.com/kovetskiy/docopt-go" "reflect" "testing" ) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5bf3a00 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/kovetskiy/docopt-go + +go 1.21.5 diff --git a/opts.go b/opts.go index 36320fb..9f91eb7 100644 --- a/opts.go +++ b/opts.go @@ -1,6 +1,7 @@ package docopt import ( + "encoding" "fmt" "reflect" "strconv" @@ -22,8 +23,8 @@ func errStrconv(key string, convErr error) error { // methods for value type conversion (bool, float64, int, string). For example, // to get an option value as an int: // -// opts, _ := docopt.ParseDoc("Usage: sleep ") -// secs, _ := opts.Int("") +// opts, _ := docopt.ParseDoc("Usage: sleep ") +// secs, _ := opts.Int("") // // Additionally, Opts.Bind allows you easily populate a struct's fields with the // values of each option value. See below for examples. @@ -31,9 +32,9 @@ func errStrconv(key string, convErr error) error { // Lastly, you can still treat Opts as a regular map, and do any type checking // and conversion that you want to yourself. For example: // -// if s, ok := opts[""].(string); ok { -// if val, err := strconv.ParseUint(s, 2, 64); err != nil { ... } -// } +// if s, ok := opts[""].(string); ok { +// if val, err := strconv.ParseUint(s, 2, 64); err != nil { ... } +// } // // Note that any non-boolean option / flag will have a string value in the // underlying map. @@ -93,20 +94,24 @@ func (o Opts) Float64(key string) (f float64, err error) { // Each key in Opts will be mapped to an exported field of the struct pointed // to by `v`, as follows: // -// abc int // Unexported field, ignored -// Abc string // Mapped from `--abc`, ``, or `abc` -// // (case insensitive) -// A string // Mapped from `-a`, `` or `a` -// // (case insensitive) -// Abc int `docopt:"XYZ"` // Mapped from `XYZ` -// Abc bool `docopt:"-"` // Mapped from `-` -// Abc bool `docopt:"-x,--xyz"` // Mapped from `-x` or `--xyz` -// // (first non-zero value found) +// abc int // Unexported field, ignored +// Abc string // Mapped from `--abc`, ``, or `abc` +// // (case insensitive) +// A string // Mapped from `-a`, `` or `a` +// // (case insensitive) +// Abc int `docopt:"XYZ"` // Mapped from `XYZ` +// Abc bool `docopt:"-"` // Mapped from `-` +// Abc bool `docopt:"-x,--xyz"` // Mapped from `-x` or `--xyz` +// // (first non-zero value found) +// Abc net.IP `docopt:"--ip"` // Mapped from `--ip` // // Tagged (annotated) fields will always be mapped first. If no field is tagged // with an option's key, Bind will try to map the option to an appropriately // named field (as above). // +// If the field implements encoding.TextUnmarshaler, the option value will be +// passed to its UnmarshalText method. +// // Bind also handles conversion to bool, float, int or string types. func (o Opts) Bind(v interface{}) error { structVal := reflect.ValueOf(v) @@ -196,6 +201,26 @@ func (o Opts) Bind(v interface{}) error { field.Set(optVal) continue } + + // try to assign to a TextUnmarshaler + if field.CanAddr() && field.Addr().CanInterface() { + if x, ok := field.Addr().Interface().(encoding.TextUnmarshaler); ok { + if text, err := o.String(k); err == nil { + err := x.UnmarshalText([]byte(text)) + if err != nil { + return newError( + "value of %q is not assignable to %q field: %w", + k, + structType.Field(i).Name, + err, + ) + } + + continue + } + } + } + // Try to convert the value and assign if able. switch field.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: @@ -208,16 +233,38 @@ func (o Opts) Bind(v interface{}) error { field.SetFloat(x) continue } + + case reflect.Slice: + if vs, ok := v.([]string); ok { + if x, ok := findUnmarshaler(field); ok { + result := reflect.MakeSlice(field.Type(), 0, len(vs)) + for _, optVal := range vs { + err := x.UnmarshalText([]byte(optVal)) + if err != nil { + return newError( + "value of %q is not assignable to %q field: %w", + k, + structType.Field(i).Name, + err, + ) + } + + rex := reflect.ValueOf(x) + + if rex.Kind() == reflect.Ptr { + rex = rex.Elem() + } + + result = reflect.Append(result, rex) + } + + field.Set(result) + + continue + } + } } - // TODO: Something clever (recursive?) with non-string slices. - // case reflect.Slice: - // if optVal.Kind() == reflect.Slice { - // for i := 0; i < optVal.Len(); i++ { - // sliceVal := optVal.Index(i) - // fmt.Printf("%v", sliceVal) - // } - // fmt.Printf("\n") - // } + return newError("value of %q is not assignable to %q field", k, structType.Field(i).Name) } @@ -227,8 +274,9 @@ func (o Opts) Bind(v interface{}) error { // isUnexportedField returns whether the field is unexported. // isUnexportedField is to avoid the bug in versions older than Go1.3. // See following links: -// https://code.google.com/p/go/issues/detail?id=7247 -// http://golang.org/ref/spec#Exported_identifiers +// +// https://code.google.com/p/go/issues/detail?id=7247 +// http://golang.org/ref/spec#Exported_identifiers func isUnexportedField(field reflect.StructField) bool { return !(field.PkgPath == "" && unicode.IsUpper(rune(field.Name[0]))) } @@ -260,5 +308,36 @@ func guessUntaggedField(key string) string { case strings.HasPrefix(key, "<") && strings.HasSuffix(key, ">"): key = key[1 : len(key)-1] } + return strings.Title(strings.ToLower(key)) } + +func findUnmarshaler(field reflect.Value) (encoding.TextUnmarshaler, bool) { + var x any + + if field.CanAddr() { + x = field.Addr() + } + + if field.CanInterface() { + x = field.Interface() + } + + underlying := reflect.TypeOf(x) + + if underlying.Kind() == reflect.Ptr { + underlying = underlying.Elem() + } + + if underlying.Kind() == reflect.Slice { + underlying = underlying.Elem() + } + + x = reflect.New(underlying).Interface() + + if unmarshaler, ok := x.(encoding.TextUnmarshaler); ok { + return unmarshaler, true + } + + return nil, false +} diff --git a/opts_test.go b/opts_test.go index 3c5c480..19cf63e 100644 --- a/opts_test.go +++ b/opts_test.go @@ -1,6 +1,7 @@ package docopt import ( + "net" "reflect" "strings" "testing" @@ -143,6 +144,7 @@ type testTypedOptions struct { V bool Number int16 + Ip net.IP Idle float32 Pointer uintptr `docopt:""` Ints []int `docopt:""` @@ -192,21 +194,28 @@ func TestBindErrors(t *testing.T) { `prog - 123 456 asd`, `mapping of "-" is not found in given struct, or is an unexported field`, }, + { + `Usage: prog [--ip=IP]`, + `prog --ip=1.2.3.4.5`, + `value of "--ip" is not assignable to "Ip" field: invalid IP address: 1.2.3.4.5`, + }, } { - argv := strings.Split(tc.command, " ")[1:] - opts, err := testParser.ParseArgs(tc.usage, argv, "") - if err != nil { - t.Fatalf("testcase: %d parse err: %q", i, err) - } - var o testTypedOptions - t.Logf("%#v\n", opts) - if err := opts.Bind(&o); err != nil { - if err.Error() != tc.expectedErr { - t.Fatalf("testcase: %d result: %q expect: %q", i, err.Error(), tc.expectedErr) + t.Run(tc.usage, func(t *testing.T) { + argv := strings.Split(tc.command, " ")[1:] + opts, err := testParser.ParseArgs(tc.usage, argv, "") + if err != nil { + t.Fatalf("testcase: %d parse err: %q", i, err) } - } else { - t.Fatal("error expected") - } + var o testTypedOptions + t.Logf("%#v\n", opts) + if err := opts.Bind(&o); err != nil { + if err.Error() != tc.expectedErr { + t.Fatalf("testcase: %d result: %q expect: %q", i, err.Error(), tc.expectedErr) + } + } else { + t.Fatal("error expected") + } + }) } } @@ -244,6 +253,22 @@ func TestBindSuccess(t *testing.T) { `Usage: prog [--help]`, `prog --help`, }, + { + `Usage: prog [--ip=X]`, + `prog`, + }, + { + `Usage: prog [--ip=X]`, + `prog --ip=127.0.0.1`, + }, + { + `Usage: prog `, + `prog 127.0.0.1`, + }, + { + `Usage: prog IP`, + `prog 127.0.0.255`, + }, } { argv := strings.Split(tc.command, " ")[1:] opts, err := testParser.ParseArgs(tc.usage, argv, "")