├── .gitattributes ├── .github ├── CODEOWNERS ├── codecov.yml ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature-request.md │ └── bug-report.md ├── workflows │ ├── test.yml │ └── lint.yml └── pull_request_template.md ├── scripts └── cover_multi.sh ├── .travis.yml ├── .gitignore ├── converters.go ├── flag.go ├── examples ├── simple_flag │ └── main.go ├── validator │ └── main.go ├── anonymous │ └── main.go ├── pflag │ └── main.go ├── flag │ └── main.go ├── kingpin │ └── main.go ├── urfave_cli │ └── main.go └── cobra │ └── main.go ├── converters_test.go ├── go.mod ├── gen ├── gkingpin │ ├── gkingpin.go │ └── gkingpin_test.go ├── gcli │ ├── gcli.go │ ├── gcliv3.go │ ├── gcli_test.go │ └── gcliv3_test.go ├── gflag │ ├── gflag.go │ └── gflag_test.go └── gpflag │ ├── gpflag.go │ └── gpflag_test.go ├── LICENSE ├── Makefile ├── camelcase_test.go ├── values_test.go ├── go.sum ├── validator └── govalidator │ ├── govalidator_test.go │ └── govalidator.go ├── camelcase.go ├── values.go ├── README.md ├── parser.go ├── parser_test.go ├── cmd └── genvalues │ └── main.go └── values.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Ensure Windows uses proper line endings 2 | *.go text eol=lf 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/about-codeowners/ 2 | # for more info about CODEOWNERS file 3 | 4 | * @urfave/cli 5 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | coverage: 3 | status: 4 | project: 5 | default: 6 | threshold: 5% 7 | patch: 8 | default: 9 | threshold: 5% 10 | ignore: 11 | - examples 12 | - scripts 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ask a question 3 | about: ask a question - assume stackoverflow's guidelines apply here 4 | title: your question title goes here 5 | labels: 'kind/question, status/triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | my question is... 11 | -------------------------------------------------------------------------------- /scripts/cover_multi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | echo "" > coverage.txt 5 | 6 | for d in $(go list ./... | grep -v vendor); do 7 | go test -race -coverprofile=profile.out -covermode=atomic $d 8 | if [ -f profile.out ]; then 9 | cat profile.out >> coverage.txt 10 | rm profile.out 11 | fi 12 | done -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | env: 4 | matrix: 5 | - GO111MODULE=on 6 | 7 | go: 8 | - 1.10.x 9 | - 1.11.x 10 | - tip 11 | 12 | before_install: 13 | - go get -t -v ./... 14 | 15 | install: 16 | - make tools 17 | 18 | script: make travis 19 | 20 | after_success: 21 | - bash <(curl -s https://codecov.io/bash) 22 | 23 | notifications: 24 | email: false 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /converters.go: -------------------------------------------------------------------------------- 1 | package sflags 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // transform s from CamelCase to flag-case 8 | func camelToFlag(s, flagDivider string) string { 9 | splitted := split(s) 10 | return strings.ToLower(strings.Join(splitted, flagDivider)) 11 | } 12 | 13 | // transform s from flag-case to CAMEL_CASE 14 | func flagToEnv(s, flagDivider, envDivider string) string { 15 | return strings.ToUpper(strings.Replace(s, flagDivider, envDivider, -1)) 16 | } 17 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | // Package sflags helps to generate flags by parsing structure 2 | package sflags 3 | 4 | // Flag structure might be used by cli/flag libraries for their flag generation. 5 | type Flag struct { 6 | Name string // name as it appears on command line 7 | Short string // optional short name 8 | EnvNames []string 9 | Usage string // help message 10 | Value Value // value as set 11 | DefValue string // default value (as text); for usage message 12 | Hidden bool 13 | Deprecated bool 14 | Required bool 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple_flag/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This packages shows how to use sflags with flag library. 4 | // Run this app with go run ./main.go -help 5 | 6 | import ( 7 | "flag" 8 | "log" 9 | "time" 10 | 11 | "github.com/urfave/sflags/gen/gflag" 12 | ) 13 | 14 | type httpConfig struct { 15 | Host string `desc:"HTTP host"` 16 | Port int 17 | SSL bool 18 | Timeout time.Duration 19 | } 20 | 21 | type config struct { 22 | HTTP httpConfig 23 | } 24 | 25 | func main() { 26 | cfg := &config{ 27 | HTTP: httpConfig{ 28 | Host: "127.0.0.1", 29 | Port: 6000, 30 | SSL: false, 31 | Timeout: 15 * time.Second, 32 | }, 33 | } 34 | err := gflag.ParseToDef(cfg) 35 | if err != nil { 36 | log.Fatalf("err: %v", err) 37 | } 38 | flag.Parse() 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | go: [stable, oldstable] 20 | 21 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 22 | runs-on: ${{ matrix.os }} 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: ${{ matrix.go }} 31 | 32 | - name: Set PATH 33 | run: echo "${GITHUB_WORKSPACE}/.local/bin" >>"${GITHUB_PATH}" 34 | 35 | - name: Run tests 36 | run: make test_v -------------------------------------------------------------------------------- /examples/validator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This packages shows how to use sflags with flag library. 4 | 5 | import ( 6 | "log" 7 | 8 | "github.com/urfave/sflags" 9 | "github.com/urfave/sflags/gen/gflag" 10 | "github.com/urfave/sflags/validator/govalidator" 11 | ) 12 | 13 | type config struct { 14 | Host string `valid:"host"` 15 | Port int `valid:"port"` 16 | } 17 | 18 | func main() { 19 | cfg := &config{ 20 | Host: "127.0.0.1", 21 | Port: 6000, 22 | } 23 | // Use gflags.ParseToDef if you want default `flag.CommandLine` 24 | fs, err := gflag.Parse(cfg, sflags.Validator(govalidator.New())) 25 | if err != nil { 26 | log.Fatalf("err: %v", err) 27 | } 28 | // if we pass wrong domain to a host flag, we'll get a error. 29 | fs.Parse([]string{ 30 | "-host", "wrong domain", 31 | "-port", "10", 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request: 8 | 9 | permissions: 10 | contents: read 11 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 12 | # pull-requests: read 13 | 14 | jobs: 15 | golangci: 16 | strategy: 17 | matrix: 18 | go: [stable] 19 | os: [ubuntu-latest, macos-latest, windows-latest] 20 | name: lint 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version: ${{ matrix.go }} 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v6 29 | with: 30 | version: v1.62 31 | - name: run CI 32 | run: make ci 33 | -------------------------------------------------------------------------------- /converters_test.go: -------------------------------------------------------------------------------- 1 | package sflags 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCamelToFlag(t *testing.T) { 10 | data := []struct { 11 | Src string 12 | Exp string 13 | }{ 14 | {"ValueValue2Value3", "value-value2-value3"}, 15 | { 16 | "ValueValue2Value3Value4Value5Value6Value7", 17 | "value-value2-value3-value4-value5-value6-value7", 18 | }, 19 | {"Value", "value"}, 20 | {"IP", "ip"}, 21 | } 22 | for _, d := range data { 23 | assert.Equal(t, d.Exp, camelToFlag(d.Src, defaultFlagDivider)) 24 | } 25 | } 26 | 27 | func TestFlagToEnv(t *testing.T) { 28 | data := []struct { 29 | Src string 30 | Exp string 31 | }{ 32 | {"value-value2-value3", "VALUE_VALUE2_VALUE3"}, 33 | {"value", "VALUE"}, 34 | } 35 | for _, d := range data { 36 | assert.Equal(t, d.Exp, flagToEnv(d.Src, defaultFlagDivider, defaultEnvDivider)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/urfave/sflags 2 | 3 | go 1.21.5 4 | 5 | require ( 6 | github.com/alecthomas/kingpin/v2 v2.4.0 7 | github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/spf13/cobra v0.0.3 10 | github.com/spf13/pflag v1.0.3 11 | github.com/stretchr/testify v1.9.0 12 | github.com/urfave/cli/v2 v2.27.5 13 | github.com/urfave/cli/v3 v3.0.0-alpha9.3 14 | golang.org/x/text v0.19.0 15 | ) 16 | 17 | require ( 18 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 19 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 23 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 24 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /examples/anonymous/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This packages shows how to use sflags with flag library. 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | 12 | "github.com/davecgh/go-spew/spew" 13 | "github.com/urfave/sflags/gen/gflag" 14 | ) 15 | 16 | type httpConfig struct { 17 | Host string 18 | } 19 | 20 | type config struct { 21 | httpConfig 22 | } 23 | 24 | func main() { 25 | cfg := &config{ 26 | httpConfig: httpConfig{ 27 | Host: "127.0.0.1", 28 | }, 29 | } 30 | // Use gflags.ParseToDef if you want default `flag.CommandLine` 31 | fs, err := gflag.Parse(cfg) 32 | if err != nil { 33 | log.Fatalf("err: %v", err) 34 | } 35 | fs.Init("", flag.ContinueOnError) 36 | fs.SetOutput(ioutil.Discard) 37 | // You should run fs.Parse(os.Args[1:]), but this is an example. 38 | err = fs.Parse([]string{ 39 | "-http-config-host", "localhost", 40 | }) 41 | if err != nil { 42 | fmt.Printf("err: %v\n", err) 43 | } 44 | fmt.Println("Usage:") 45 | fs.SetOutput(os.Stdout) 46 | fs.PrintDefaults() 47 | fmt.Printf("cfg: %s\n", spew.Sdump(cfg)) 48 | } 49 | -------------------------------------------------------------------------------- /gen/gkingpin/gkingpin.go: -------------------------------------------------------------------------------- 1 | package gkingpin 2 | 3 | import ( 4 | "unicode/utf8" 5 | 6 | "github.com/alecthomas/kingpin/v2" 7 | "github.com/urfave/sflags" 8 | ) 9 | 10 | type flagger interface { 11 | Flag(name, help string) *kingpin.FlagClause 12 | } 13 | 14 | // GenerateTo takes a list of sflag.Flag, 15 | // that are parsed from some config structure, and put it to dst. 16 | func GenerateTo(src []*sflags.Flag, dst flagger) { 17 | for _, srcFlag := range src { 18 | flag := dst.Flag(srcFlag.Name, srcFlag.Usage) 19 | flag.SetValue(srcFlag.Value) 20 | if len(srcFlag.EnvNames) > 0 && srcFlag.EnvNames[0] != "" { 21 | flag.Envar(srcFlag.EnvNames[0]) 22 | } 23 | if srcFlag.Hidden { 24 | flag.Hidden() 25 | } 26 | if srcFlag.Required { 27 | flag.Required() 28 | } 29 | if srcFlag.Short != "" { 30 | r, _ := utf8.DecodeRuneInString(srcFlag.Short) 31 | if r != utf8.RuneError { 32 | flag.Short(r) 33 | } 34 | } 35 | 36 | } 37 | } 38 | 39 | // ParseTo parses cfg, that is a pointer to some structure, 40 | // and puts it to dst. 41 | func ParseTo(cfg interface{}, dst flagger, optFuncs ...sflags.OptFunc) error { 42 | flags, err := sflags.ParseStruct(cfg, optFuncs...) 43 | if err != nil { 44 | return err 45 | } 46 | GenerateTo(flags, dst) 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: feature request 3 | about: Suggest an improvement to go into sflags 4 | title: 'your feature title goes here' 5 | labels: 'type/feature, status/triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Checklist 11 | 12 | * [ ] Are you running the latest v3 release? The list of releases is [here](https://github.com/urfave/sflags/releases). 13 | * [ ] Did you perform a search about this feature? Here's the [GitHub guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. 14 | 15 | ## What problem does this solve? 16 | 17 | A clear and concise description of what problem this feature would solve. For example: 18 | 19 | - needing to type out the full flag name takes a long time, so I 20 | would like to suggest adding auto-complete 21 | - I use (osx, windows, linux) and would like support for (some 22 | existing feature) to be extended to my platform 23 | - the terminal output for a particular error case is confusing, and 24 | I think it could be improved 25 | 26 | ## Solution description 27 | 28 | A detailed description of what you want to happen. 29 | 30 | ## Describe alternatives you've considered 31 | 32 | A clear and concise description of any alternative solutions or 33 | features you've considered. 34 | -------------------------------------------------------------------------------- /gen/gcli/gcli.go: -------------------------------------------------------------------------------- 1 | package gcli 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | "github.com/urfave/sflags" 6 | ) 7 | 8 | // GenerateTo takes a list of sflag.Flag, 9 | // that are parsed from some config structure, and put it to dst. 10 | func GenerateTo(src []*sflags.Flag, dst *[]cli.Flag) { 11 | for _, srcFlag := range src { 12 | name := srcFlag.Name 13 | var aliases []string 14 | if srcFlag.Short != "" { 15 | aliases = append(aliases, srcFlag.Short) 16 | } 17 | *dst = append(*dst, &cli.GenericFlag{ 18 | Name: name, 19 | EnvVars: srcFlag.EnvNames, 20 | Aliases: aliases, 21 | Hidden: srcFlag.Hidden, 22 | Usage: srcFlag.Usage, 23 | Value: srcFlag.Value, 24 | Required: srcFlag.Required, 25 | }) 26 | } 27 | } 28 | 29 | // ParseTo parses cfg, that is a pointer to some structure, 30 | // and puts it to dst. 31 | func ParseTo(cfg interface{}, dst *[]cli.Flag, optFuncs ...sflags.OptFunc) error { 32 | flags, err := sflags.ParseStruct(cfg, optFuncs...) 33 | if err != nil { 34 | return err 35 | } 36 | GenerateTo(flags, dst) 37 | return nil 38 | } 39 | 40 | // Parse parses cfg, that is a pointer to some structure, 41 | // puts it to the new flag.FlagSet and returns it. 42 | func Parse(cfg interface{}, optFuncs ...sflags.OptFunc) ([]cli.Flag, error) { 43 | flags := make([]cli.Flag, 0) 44 | err := ParseTo(cfg, &flags, optFuncs...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return flags, nil 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016, Slava Bakhmutov 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /gen/gflag/gflag.go: -------------------------------------------------------------------------------- 1 | package gflag 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | "github.com/urfave/sflags" 8 | ) 9 | 10 | // flagSet describes interface, 11 | // that's implemented by flag library and required by sflags. 12 | type flagSet interface { 13 | Var(value flag.Value, name string, usage string) 14 | } 15 | 16 | var _ flagSet = (*flag.FlagSet)(nil) 17 | 18 | // GenerateTo takes a list of sflag.Flag, 19 | // that are parsed from some config structure, and put it to dst. 20 | func GenerateTo(src []*sflags.Flag, dst flagSet) { 21 | for _, srcFlag := range src { 22 | dst.Var(srcFlag.Value, srcFlag.Name, srcFlag.Usage) 23 | } 24 | } 25 | 26 | // ParseTo parses cfg, that is a pointer to some structure, 27 | // and puts it to dst. 28 | func ParseTo(cfg interface{}, dst flagSet, optFuncs ...sflags.OptFunc) error { 29 | flags, err := sflags.ParseStruct(cfg, optFuncs...) 30 | if err != nil { 31 | return err 32 | } 33 | GenerateTo(flags, dst) 34 | return nil 35 | } 36 | 37 | // Parse parses cfg, that is a pointer to some structure, 38 | // puts it to the new flag.FlagSet and returns it. 39 | func Parse(cfg interface{}, optFuncs ...sflags.OptFunc) (*flag.FlagSet, error) { 40 | fs := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 41 | err := ParseTo(cfg, fs, optFuncs...) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return fs, nil 46 | } 47 | 48 | // ParseToDef parses cfg, that is a pointer to some structure and 49 | // puts it to the default flag.CommandLine. 50 | func ParseToDef(cfg interface{}, optFuncs ...sflags.OptFunc) error { 51 | err := ParseTo(cfg, flag.CommandLine, optFuncs...) 52 | if err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /examples/pflag/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This packages shows how to use sflags with pflag library. 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "net" 9 | "regexp" 10 | "time" 11 | 12 | "github.com/davecgh/go-spew/spew" 13 | "github.com/urfave/sflags" 14 | "github.com/urfave/sflags/gen/gpflag" 15 | ) 16 | 17 | type httpConfig struct { 18 | Host string `desc:"HTTP host"` 19 | Port int 20 | SSL bool 21 | Timeout time.Duration 22 | Addr *net.TCPAddr 23 | } 24 | 25 | type config struct { 26 | HTTP httpConfig 27 | Regexp *regexp.Regexp 28 | Count sflags.Counter 29 | OldFlag string `flag:",deprecated" desc:"use other flag instead"` 30 | HiddenFlag string `flag:",hidden"` 31 | } 32 | 33 | func main() { 34 | cfg := &config{ 35 | HTTP: httpConfig{ 36 | Host: "127.0.0.1", 37 | Port: 6000, 38 | SSL: false, 39 | Timeout: 15 * time.Second, 40 | Addr: &net.TCPAddr{ 41 | IP: net.ParseIP("127.0.0.1"), 42 | Port: 4000, 43 | }, 44 | }, 45 | Count: 12, 46 | Regexp: regexp.MustCompile("abc"), 47 | } 48 | 49 | fs, err := gpflag.Parse(cfg) 50 | if err != nil { 51 | log.Fatalf("err: %v", err) 52 | } 53 | err = fs.Parse([]string{ 54 | "--old-flag", "old", 55 | }) 56 | if err != nil { 57 | fmt.Printf("err: %v", err) 58 | } 59 | err = fs.Parse([]string{ 60 | "--count=10", 61 | "--http-host", "localhost", 62 | "--http-port", "9000", 63 | "--http-ssl", 64 | "--http-timeout", "30s", 65 | "--http-addr", "google.com:8000", 66 | "--regexp", "ddfd", 67 | "--count", "--count", 68 | }) 69 | if err != nil { 70 | fmt.Printf("err: %v", err) 71 | } 72 | fmt.Printf("usage:\n%s\n", fs.FlagUsages()) 73 | fmt.Printf("cfg: %s\n", spew.Sdump(cfg)) 74 | } 75 | -------------------------------------------------------------------------------- /examples/flag/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This packages shows how to use sflags with flag library. 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "net" 9 | "os" 10 | "regexp" 11 | "time" 12 | 13 | "github.com/davecgh/go-spew/spew" 14 | "github.com/urfave/sflags" 15 | "github.com/urfave/sflags/gen/gflag" 16 | ) 17 | 18 | type httpConfig struct { 19 | Host string `desc:"HTTP host"` 20 | Port int 21 | SSL bool 22 | Timeout time.Duration 23 | Addr *net.TCPAddr 24 | Methods map[string]int64 `desc:"HTTP Methods"` 25 | } 26 | 27 | type config struct { 28 | HTTP httpConfig 29 | Regexp *regexp.Regexp 30 | Count sflags.Counter 31 | } 32 | 33 | func main() { 34 | cfg := &config{ 35 | HTTP: httpConfig{ 36 | Host: "127.0.0.1", 37 | Port: 6000, 38 | SSL: false, 39 | Timeout: 15 * time.Second, 40 | Addr: &net.TCPAddr{ 41 | IP: net.ParseIP("127.0.0.1"), 42 | Port: 4000, 43 | }, 44 | }, 45 | Regexp: regexp.MustCompile("smth"), 46 | Count: 10, 47 | } 48 | // Use gflags.ParseToDef if you want default `flag.CommandLine` 49 | fs, err := gflag.Parse(cfg) 50 | if err != nil { 51 | log.Fatalf("err: %v", err) 52 | } 53 | // You should run fs.Parse(os.Args[1:]), but this is an example. 54 | err = fs.Parse([]string{ 55 | "-count=20", 56 | "-http-host", "localhost", 57 | "-http-port", "9000", 58 | "-http-ssl", 59 | "-http-timeout", "30s", 60 | "-http-addr", "google.com:8000", 61 | "-regexp", "ddfd", 62 | "-count", "-count", 63 | "-http-methods", "post:15", 64 | "-http-methods", "get:25", 65 | }) 66 | if err != nil { 67 | fmt.Printf("err: %v", err) 68 | } 69 | fmt.Println("Usage:") 70 | fs.SetOutput(os.Stdout) 71 | fs.PrintDefaults() 72 | fmt.Printf("cfg: %s\n", spew.Sdump(cfg)) 73 | } 74 | -------------------------------------------------------------------------------- /examples/kingpin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This packages shows how to use sflags with kingpin library. 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "net" 9 | "regexp" 10 | "time" 11 | 12 | "github.com/alecthomas/kingpin/v2" 13 | "github.com/davecgh/go-spew/spew" 14 | "github.com/urfave/sflags" 15 | "github.com/urfave/sflags/gen/gkingpin" 16 | ) 17 | 18 | type httpConfig struct { 19 | Host string `desc:"HTTP host2"` 20 | Port int `flag:"port p"` 21 | SSL bool 22 | Timeout time.Duration 23 | Addr *net.TCPAddr 24 | } 25 | 26 | type config struct { 27 | HTTP httpConfig 28 | Regexp *regexp.Regexp 29 | Count sflags.Counter 30 | HiddenFlag string `flag:",hidden"` 31 | } 32 | 33 | func main() { 34 | cfg := &config{ 35 | HTTP: httpConfig{ 36 | Host: "127.0.0.1", 37 | Port: 6000, 38 | SSL: false, 39 | Timeout: 15 * time.Second, 40 | Addr: &net.TCPAddr{ 41 | IP: net.ParseIP("127.0.0.1"), 42 | Port: 4000, 43 | }, 44 | }, 45 | Count: 12, 46 | Regexp: regexp.MustCompile("abc"), 47 | } 48 | 49 | app := kingpin.New("testApp", "") 50 | app.Terminate(nil) 51 | 52 | err := gkingpin.ParseTo(cfg, app, sflags.Prefix("kingpin.")) 53 | if err != nil { 54 | log.Fatalf("err: %v", err) 55 | } 56 | 57 | // print usage 58 | _, err = app.Parse([]string{"--help"}) 59 | if err != nil { 60 | fmt.Printf("err: %v", err) 61 | } 62 | _, err = app.Parse([]string{ 63 | "--kingpin.http-host", "localhost", 64 | "-p", "9000", 65 | "--kingpin.http-ssl", 66 | "--kingpin.http-timeout", "30s", 67 | "--kingpin.http-addr", "google.com:8000", 68 | "--kingpin.regexp", "ddfd", 69 | "--kingpin.count", "--kingpin.count", 70 | "--kingpin.hidden-flag", "hidden_value", 71 | }) 72 | if err != nil { 73 | fmt.Printf("err: %v", err) 74 | } 75 | fmt.Printf("\ncfg: %s\n", spew.Sdump(cfg)) 76 | } 77 | -------------------------------------------------------------------------------- /examples/urfave_cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This packages shows how to use sflags with urfave/cli library. 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "net" 9 | "regexp" 10 | "time" 11 | 12 | "github.com/davecgh/go-spew/spew" 13 | "github.com/urfave/cli/v2" 14 | "github.com/urfave/sflags" 15 | "github.com/urfave/sflags/gen/gcli" 16 | ) 17 | 18 | type httpConfig struct { 19 | Host string ` desc:"HTTP host"` 20 | Port int `flag:"port p"` 21 | SSL bool 22 | Timeout time.Duration 23 | Addr *net.TCPAddr 24 | } 25 | 26 | type config struct { 27 | HTTP httpConfig 28 | Regexp *regexp.Regexp 29 | Count sflags.Counter 30 | HiddenFlag string `flag:",hidden"` 31 | } 32 | 33 | func main() { 34 | cfg := &config{ 35 | HTTP: httpConfig{ 36 | Host: "127.0.0.1", 37 | Port: 6000, 38 | SSL: false, 39 | Timeout: 15 * time.Second, 40 | Addr: &net.TCPAddr{ 41 | IP: net.ParseIP("127.0.0.1"), 42 | Port: 4000, 43 | }, 44 | }, 45 | Count: 12, 46 | Regexp: regexp.MustCompile("abc"), 47 | } 48 | 49 | flags, err := gcli.Parse(cfg) 50 | if err != nil { 51 | log.Fatalf("err: %v", err) 52 | } 53 | cliApp := cli.NewApp() 54 | cliApp.Action = func(c *cli.Context) error { 55 | return nil 56 | } 57 | cliApp.Flags = flags 58 | // print usage 59 | err = cliApp.Run([]string{"cliApp", "--help"}) 60 | if err != nil { 61 | fmt.Printf("err: %v", err) 62 | } 63 | err = cliApp.Run([]string{ 64 | "cliApp", 65 | "--count=10", 66 | "--http-host", "localhost", 67 | "-p", "9000", 68 | "--http-ssl", 69 | "--http-timeout", "30s", 70 | "--http-addr", "google.com:8000", 71 | "--regexp", "ddfd", 72 | "--count", "--count", 73 | "--hidden-flag", "hidden_value", 74 | }) 75 | if err != nil { 76 | fmt.Printf("err: %v", err) 77 | } 78 | fmt.Printf("\ncfg: %s\n", spew.Sdump(cfg)) 79 | } 80 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## What type of PR is this? 8 | 9 | _(REQUIRED)_ 10 | 11 | 14 | 15 | - bug 16 | - cleanup 17 | - documentation 18 | - feature 19 | 20 | ## What this PR does / why we need it: 21 | 22 | _(REQUIRED)_ 23 | 24 | 31 | 32 | ## Which issue(s) this PR fixes: 33 | 34 | _(REQUIRED)_ 35 | 36 | 43 | 44 | ## Special notes for your reviewer: 45 | 46 | _(fill-in or delete this section)_ 47 | 48 | 52 | 53 | ## Testing 54 | 55 | _(fill-in or delete this section)_ 56 | 57 | 60 | 61 | ## Release Notes 62 | 63 | _(REQUIRED)_ 64 | 72 | 73 | ```release-note 74 | 75 | ``` 76 | -------------------------------------------------------------------------------- /gen/gcli/gcliv3.go: -------------------------------------------------------------------------------- 1 | package gcli 2 | 3 | import ( 4 | "github.com/urfave/cli/v3" 5 | "github.com/urfave/sflags" 6 | ) 7 | 8 | type boolFlag interface { 9 | IsBoolFlag() bool 10 | } 11 | 12 | type value struct { 13 | v sflags.Value 14 | } 15 | 16 | func (v value) Get() any { 17 | return v.v 18 | } 19 | 20 | func (v value) Set(s string) error { 21 | return v.v.Set(s) 22 | } 23 | 24 | func (v value) String() string { 25 | return v.v.String() 26 | } 27 | 28 | func (v value) IsBoolFlag() bool { 29 | b, ok := v.v.(boolFlag) 30 | return ok && b.IsBoolFlag() 31 | } 32 | 33 | // GenerateToV3 takes a list of sflag.Flag, 34 | // that are parsed from some config structure, and put it to dst. 35 | func GenerateToV3(src []*sflags.Flag, dst *[]cli.Flag) { 36 | for _, srcFlag := range src { 37 | name := srcFlag.Name 38 | var aliases []string 39 | if srcFlag.Short != "" { 40 | aliases = append(aliases, srcFlag.Short) 41 | } 42 | *dst = append(*dst, &cli.GenericFlag{ 43 | Name: name, 44 | Sources: cli.EnvVars(srcFlag.EnvNames...), 45 | Aliases: aliases, 46 | Hidden: srcFlag.Hidden, 47 | Usage: srcFlag.Usage, 48 | Value: &value{ 49 | v: srcFlag.Value, 50 | }, 51 | Required: srcFlag.Required, 52 | }) 53 | } 54 | } 55 | 56 | // ParseToV3 parses cfg, that is a pointer to some structure, 57 | // and puts it to dst. 58 | func ParseToV3(cfg interface{}, dst *[]cli.Flag, optFuncs ...sflags.OptFunc) error { 59 | flags, err := sflags.ParseStruct(cfg, optFuncs...) 60 | if err != nil { 61 | return err 62 | } 63 | GenerateToV3(flags, dst) 64 | return nil 65 | } 66 | 67 | // ParseV3 parses cfg, that is a pointer to some structure, 68 | // puts it to the new flag.FlagSet and returns it. 69 | func ParseV3(cfg interface{}, optFuncs ...sflags.OptFunc) ([]cli.Flag, error) { 70 | flags := make([]cli.Flag, 0) 71 | err := ParseToV3(cfg, &flags, optFuncs...) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return flags, nil 76 | } 77 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all test test_v generate lint vet fmt coverage check check-fast prepare race 2 | 3 | NO_COLOR=\033[0m 4 | OK_COLOR=\033[32;01m 5 | ERROR_COLOR=\033[31;01m 6 | WARN_COLOR=\033[33;01m 7 | PKGSDIRS=$(shell find -L . -type f -name "*.go") 8 | 9 | all: prepare 10 | 11 | ci: vet check test_v coverage 12 | 13 | coverage: 14 | @echo "$(OK_COLOR)Generate coverage$(NO_COLOR)" 15 | @./scripts/cover_multi.sh 16 | 17 | prepare: generate fmt vet lint check test race 18 | 19 | test_v: 20 | @echo "$(OK_COLOR)Test packages$(NO_COLOR)" 21 | @go test -cover -v ./... 22 | 23 | test: 24 | @echo "$(OK_COLOR)Test packages$(NO_COLOR)" 25 | @go test -cover ./... 26 | 27 | lint: 28 | @echo "$(OK_COLOR)Run lint$(NO_COLOR)" 29 | @test -z "$$(golint -min_confidence 0.3 ./... | tee /dev/stderr)" 30 | 31 | check: 32 | @echo "$(OK_COLOR)Run golangci-lint$(NO_COLOR)" 33 | @golangci-lint run --no-config --exclude-use-default=true --max-same-issues=10 --disable=gosimple --enable=staticcheck --enable=unused --enable=goconst --enable=misspell --enable=unparam --enable=goimports --disable=errcheck --disable=ineffassign --disable=gocyclo --disable=gosec 34 | 35 | vet: 36 | @echo "$(OK_COLOR)Run vet$(NO_COLOR)" 37 | @go vet ./... 38 | 39 | race: 40 | @echo "$(OK_COLOR)Test for races$(NO_COLOR)" 41 | @go test -race . 42 | 43 | fmt: 44 | @echo "$(OK_COLOR)Formatting$(NO_COLOR)" 45 | @echo $(PKGSDIRS) | xargs goimports -w 46 | 47 | info: 48 | depscheck -totalonly -tests . 49 | golocc --no-vendor ./... 50 | 51 | generate: 52 | @echo "$(OK_COLOR)Go generate$(NO_COLOR)" 53 | @go generate 54 | 55 | tools: 56 | @echo "$(OK_COLOR)Install tools$(NO_COLOR)" 57 | go get -u github.com/warmans/golocc 58 | go get -u github.com/divan/depscheck 59 | GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lint 60 | cd ${GOPATH}/src/github.com/golangci/golangci-lint/cmd/golangci-lint \ 61 | && go install -ldflags "-X 'main.version=$(git describe --tags)' -X 'main.commit=$(git rev-parse --short HEAD)' -X 'main.date=$(date)'" 62 | -------------------------------------------------------------------------------- /gen/gpflag/gpflag.go: -------------------------------------------------------------------------------- 1 | package gpflag 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/pflag" 7 | "github.com/urfave/sflags" 8 | ) 9 | 10 | // flagSet describes interface, 11 | // that's implemented by pflag library and required by sflags. 12 | type flagSet interface { 13 | VarPF(value pflag.Value, name, shorthand, usage string) *pflag.Flag 14 | } 15 | 16 | var _ flagSet = (*pflag.FlagSet)(nil) 17 | 18 | // GenerateTo takes a list of sflag.Flag, 19 | // that are parsed from some config structure, and put it to dst. 20 | func GenerateTo(src []*sflags.Flag, dst flagSet) { 21 | for _, srcFlag := range src { 22 | flag := dst.VarPF(srcFlag.Value, srcFlag.Name, srcFlag.Short, srcFlag.Usage) 23 | if boolFlag, casted := srcFlag.Value.(sflags.BoolFlag); casted && boolFlag.IsBoolFlag() { 24 | // pflag uses -1 in this case, 25 | // we will use the same behaviour as in flag library 26 | flag.NoOptDefVal = "true" 27 | } 28 | flag.Hidden = srcFlag.Hidden 29 | if srcFlag.Deprecated { 30 | // we use Usage as Deprecated message for a pflag 31 | flag.Deprecated = srcFlag.Usage 32 | if flag.Deprecated == "" { 33 | flag.Deprecated = "Deprecated" 34 | } 35 | } 36 | } 37 | } 38 | 39 | // ParseTo parses cfg, that is a pointer to some structure, 40 | // and puts it to dst. 41 | func ParseTo(cfg interface{}, dst flagSet, optFuncs ...sflags.OptFunc) error { 42 | flags, err := sflags.ParseStruct(cfg, optFuncs...) 43 | if err != nil { 44 | return err 45 | } 46 | GenerateTo(flags, dst) 47 | return nil 48 | } 49 | 50 | // Parse parses cfg, that is a pointer to some structure, 51 | // puts it to the new pflag.FlagSet and returns it. 52 | func Parse(cfg interface{}, optFuncs ...sflags.OptFunc) (*pflag.FlagSet, error) { 53 | fs := pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) 54 | err := ParseTo(cfg, fs, optFuncs...) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return fs, nil 59 | } 60 | 61 | // ParseToDef parses cfg, that is a pointer to some structure and 62 | // puts it to the default pflag.CommandLine. 63 | func ParseToDef(cfg interface{}, optFuncs ...sflags.OptFunc) error { 64 | err := ParseTo(cfg, pflag.CommandLine, optFuncs...) 65 | if err != nil { 66 | return err 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: bug report 3 | about: Create a report to help us fix bugs 4 | title: 'your bug title goes here' 5 | labels: 'kind/bug, status/triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## My urfave/sflags version is 11 | 12 | _**( Put the version of urfave/sflags that you are using here )**_ 13 | 14 | ## Checklist 15 | 16 | - [ ] Are you running the latest release? The list of releases is [here](https://github.com/urfave/sflags/releases). 17 | - [ ] Did you perform a search about this problem? Here's the [GitHub guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. 18 | 19 | ## Dependency Management 20 | 21 | 24 | 25 | - My project is using go modules. 26 | - My project is using vendoring. 27 | - My project is automatically downloading the latest version. 28 | - I am unsure of what my dependency management setup is. 29 | 30 | ## Describe the bug 31 | 32 | A clear and concise description of what the bug is. 33 | 34 | ## To reproduce 35 | 36 | Describe the steps or code required to reproduce the behavior 37 | 38 | ## Observed behavior 39 | 40 | What did you see happen immediately after the reproduction steps 41 | above? 42 | 43 | ## Expected behavior 44 | 45 | What would you have expected to happen immediately after the 46 | reproduction steps above? 47 | 48 | ## Additional context 49 | 50 | Add any other context about the problem here. 51 | 52 | If the issue relates to a specific open source GitHub repo, please 53 | link that repo here. 54 | 55 | If you can reproduce this issue with a public CI system, please 56 | link a failing build here. 57 | 58 | ## Want to fix this yourself? 59 | 60 | We'd love to have more contributors on this project! If the fix for 61 | this bug is easily explained and very small, feel free to create a 62 | pull request for it. You'll want to base the PR off the `v1` 63 | branch, all `v1` bug fix releases will be made from that branch. 64 | 65 | ## Run `go version` and paste its output here 66 | 67 | ``` 68 | # paste `go version` output in here 69 | ``` 70 | 71 | ## Run `go env` and paste its output here 72 | 73 | ``` 74 | # paste `go env` output in here 75 | ``` 76 | -------------------------------------------------------------------------------- /examples/cobra/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This packages shows how to use sflags with cobra library. 4 | // cobra packages uses pflag. 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net" 10 | "regexp" 11 | "time" 12 | 13 | "github.com/davecgh/go-spew/spew" 14 | "github.com/spf13/cobra" 15 | "github.com/urfave/sflags" 16 | "github.com/urfave/sflags/gen/gpflag" 17 | ) 18 | 19 | type httpConfig struct { 20 | Host string `desc:"HTTP host"` 21 | Port int 22 | SSL bool 23 | Timeout time.Duration 24 | Addr *net.TCPAddr 25 | } 26 | 27 | type config struct { 28 | HTTP httpConfig 29 | Regexp *regexp.Regexp 30 | Count sflags.Counter 31 | OldFlag string `flag:",deprecated" desc:"use other flag instead"` 32 | HiddenFlag string `flag:",hidden"` 33 | } 34 | 35 | func main() { 36 | // set default values to config 37 | cfg := &config{ 38 | HTTP: httpConfig{ 39 | Host: "127.0.0.1", 40 | Port: 6000, 41 | SSL: false, 42 | Timeout: 15 * time.Second, 43 | Addr: &net.TCPAddr{ 44 | IP: net.ParseIP("127.0.0.1"), 45 | Port: 4000, 46 | }, 47 | }, 48 | Count: 12, 49 | Regexp: regexp.MustCompile("abc"), 50 | } 51 | cmd := &cobra.Command{ 52 | Run: func(cmd *cobra.Command, args []string) { 53 | // Do Stuff Here 54 | }, 55 | } 56 | err := gpflag.ParseTo(cfg, cmd.Flags()) 57 | if err != nil { 58 | log.Fatalf("err: %v", err) 59 | } 60 | // In you program use cmd.Execute(), but this is just an example. 61 | //if err := cmd.Execute(); err != nil { 62 | // fmt.Println(err) 63 | // os.Exit(-1) 64 | //} 65 | 66 | // this should show a message that old-flag is deprecated 67 | err = cmd.ParseFlags([]string{ 68 | "--old-flag", "old", 69 | }) 70 | if err != nil { 71 | fmt.Printf("err: %v", err) 72 | } 73 | // parse normal command line arguments to see a changes in cfg structure 74 | err = cmd.ParseFlags([]string{ 75 | "--count=10", 76 | "--http-host", "localhost", 77 | "--http-port", "9000", 78 | "--http-ssl", 79 | "--http-timeout", "30s", 80 | "--http-addr", "google.com:8000", 81 | "--regexp", "ddfd", 82 | "--count", "--count", 83 | }) 84 | if err != nil { 85 | fmt.Printf("err: %v", err) 86 | } 87 | cmd.Usage() 88 | //fmt.Printf("usage:\n%s\n", cmd.FlagUsages()) 89 | fmt.Printf("\ncfg:\n %s", spew.Sdump(cfg)) 90 | } 91 | -------------------------------------------------------------------------------- /camelcase_test.go: -------------------------------------------------------------------------------- 1 | // This part was taken from the https://github.com/fatih/camelcase package 2 | // 3 | // The MIT License (MIT) 4 | // 5 | // # Copyright (c) 2015 Fatih Arslan 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | // this software and associated documentation files (the "Software"), to deal in 9 | // the Software without restriction, including without limitation the rights to 10 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | // the Software, and to permit persons to whom the Software is furnished to do so, 12 | // subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in all 15 | // copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | package sflags 24 | 25 | import ( 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | ) 30 | 31 | func TestCamelCase(t *testing.T) { 32 | tests := []struct { 33 | src string 34 | exp []string 35 | }{ 36 | {"", []string{}}, 37 | {"lowercase", []string{"lowercase"}}, 38 | {"Class", []string{"Class"}}, 39 | {"MyClass", []string{"My", "Class"}}, 40 | {"MyC", []string{"My", "C"}}, 41 | {"HTML", []string{"HTML"}}, 42 | {"PDFLoader", []string{"PDF", "Loader"}}, 43 | {"AString", []string{"A", "String"}}, 44 | {"SimpleXMLParser", []string{"Simple", "XML", "Parser"}}, 45 | {"vimRPCPlugin", []string{"vim", "RPC", "Plugin"}}, 46 | {"GL11Version", []string{"GL11", "Version"}}, 47 | {"99Bottles", []string{"99", "Bottles"}}, 48 | {"May5", []string{"May5"}}, 49 | {"BFG9000", []string{"BFG9000"}}, 50 | {"BöseÜberraschung", []string{"Böse", "Überraschung"}}, 51 | {"Two spaces", []string{"Two", " ", "spaces"}}, 52 | {"BadUTF8\xe2\xe2\xa1", []string{"BadUTF8\xe2\xe2\xa1"}}, 53 | } 54 | 55 | for _, test := range tests { 56 | assert.Equal(t, test.exp, split(test.src)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /values_test.go: -------------------------------------------------------------------------------- 1 | package sflags 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCounter_Set(t *testing.T) { 11 | var err error 12 | initial := 0 13 | counter := (*Counter)(&initial) 14 | 15 | assert.Equal(t, 0, initial) 16 | assert.Equal(t, "0", counter.String()) 17 | assert.Equal(t, 0, counter.Get()) 18 | assert.Equal(t, "count", counter.Type()) 19 | assert.Equal(t, true, counter.IsBoolFlag()) 20 | assert.Equal(t, true, counter.IsCumulative()) 21 | 22 | err = counter.Set("") 23 | assert.NoError(t, err) 24 | assert.Equal(t, 1, initial) 25 | assert.Equal(t, "1", counter.String()) 26 | 27 | err = counter.Set("10") 28 | assert.NoError(t, err) 29 | assert.Equal(t, 10, initial) 30 | assert.Equal(t, "10", counter.String()) 31 | 32 | err = counter.Set("-1") 33 | assert.NoError(t, err) 34 | assert.Equal(t, 11, initial) 35 | assert.Equal(t, "11", counter.String()) 36 | 37 | err = counter.Set("b") 38 | assert.Error(t, err, "strconv.ParseInt: parsing \"b\": invalid syntax") 39 | assert.Equal(t, 11, initial) 40 | assert.Equal(t, "11", counter.String()) 41 | } 42 | 43 | func TestBoolValue_IsBoolFlag(t *testing.T) { 44 | b := &boolValue{} 45 | assert.True(t, b.IsBoolFlag()) 46 | } 47 | 48 | func TestValidateValue_IsBoolFlag(t *testing.T) { 49 | boolV := true 50 | v := &validateValue{Value: newBoolValue(&boolV)} 51 | assert.True(t, v.IsBoolFlag()) 52 | 53 | v = &validateValue{Value: newStringValue(strP("stringValue"))} 54 | assert.False(t, v.IsBoolFlag()) 55 | } 56 | 57 | func TestValidateValue_IsCumulative(t *testing.T) { 58 | v := &validateValue{Value: newStringValue(strP("stringValue"))} 59 | assert.False(t, v.IsCumulative()) 60 | 61 | v = &validateValue{Value: newStringSliceValue(&[]string{})} 62 | assert.True(t, v.IsCumulative()) 63 | } 64 | 65 | func TestValidateValue_String(t *testing.T) { 66 | v := &validateValue{Value: newStringValue(strP("stringValue"))} 67 | assert.Equal(t, "stringValue", v.String()) 68 | 69 | v = &validateValue{Value: nil} 70 | assert.Equal(t, "", v.String()) 71 | } 72 | 73 | func TestValidateValue_Set(t *testing.T) { 74 | sV := strP("stringValue") 75 | v := &validateValue{Value: newStringValue(sV)} 76 | assert.NoError(t, v.Set("newVal")) 77 | assert.Equal(t, "newVal", *sV) 78 | 79 | v.validateFunc = func(val string) error { 80 | return nil 81 | } 82 | assert.NoError(t, v.Set("newVal")) 83 | 84 | v.validateFunc = func(val string) error { 85 | return fmt.Errorf("invalid %s", val) 86 | } 87 | assert.EqualError(t, v.Set("newVal"), "invalid newVal") 88 | } 89 | -------------------------------------------------------------------------------- /gen/gflag/gflag_test.go: -------------------------------------------------------------------------------- 1 | package gflag 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | "github.com/urfave/sflags" 12 | ) 13 | 14 | type cfg1 struct { 15 | StringValue1 string 16 | StringValue2 string `flag:"string-value-two"` 17 | 18 | CounterValue1 sflags.Counter 19 | 20 | StringSliceValue1 []string 21 | } 22 | 23 | func TestParse(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | 27 | cfg interface{} 28 | args []string 29 | expCfg interface{} 30 | expErr1 error // sflag Parse error 31 | expErr2 error // flag Parse error 32 | }{ 33 | { 34 | name: "Test cfg1", 35 | cfg: &cfg1{ 36 | StringValue1: "string_value1_value", 37 | StringValue2: "string_value2_value", 38 | 39 | CounterValue1: 1, 40 | 41 | StringSliceValue1: []string{"one", "two"}, 42 | }, 43 | expCfg: &cfg1{ 44 | StringValue1: "string_value1_value2", 45 | StringValue2: "string_value2_value2", 46 | 47 | CounterValue1: 3, 48 | 49 | StringSliceValue1: []string{ 50 | "one2", "two2", "three", "4"}, 51 | }, 52 | args: []string{ 53 | "-string-value1", "string_value1_value2", 54 | "-string-value-two", "string_value2_value2", 55 | "-counter-value1", "-counter-value1", 56 | "-string-slice-value1", "one2", 57 | "-string-slice-value1", "two2", 58 | "-string-slice-value1", "three,4", 59 | }, 60 | }, 61 | { 62 | name: "Test cfg1 no args", 63 | cfg: &cfg1{ 64 | StringValue1: "string_value1_value", 65 | StringValue2: "", 66 | }, 67 | expCfg: &cfg1{ 68 | StringValue1: "string_value1_value", 69 | StringValue2: "", 70 | }, 71 | args: []string{}, 72 | }, 73 | { 74 | name: "Test cfg1 without default values", 75 | cfg: &cfg1{}, 76 | expCfg: &cfg1{ 77 | StringValue1: "string_value1_value2", 78 | StringValue2: "string_value2_value2", 79 | 80 | CounterValue1: 3, 81 | }, 82 | args: []string{ 83 | "-string-value1", "string_value1_value2", 84 | "-string-value-two", "string_value2_value2", 85 | "-counter-value1=2", "-counter-value1", 86 | }, 87 | }, 88 | { 89 | name: "Test bad cfg value", 90 | cfg: "bad config", 91 | expErr1: errors.New("object must be a pointer to struct or interface"), 92 | }, 93 | } 94 | for _, test := range tests { 95 | t.Run(test.name, func(t *testing.T) { 96 | fs, err := Parse(test.cfg) 97 | require.Equal(t, test.expErr1, err) 98 | if err != nil { 99 | return 100 | } 101 | err = fs.Parse(test.args) 102 | require.Equal(t, test.expErr2, err) 103 | if err != nil { 104 | return 105 | } 106 | assert.Equal(t, test.expCfg, test.cfg) 107 | }) 108 | } 109 | } 110 | 111 | func TestParseToDef(t *testing.T) { 112 | oldCommandLine := flag.CommandLine 113 | defer func() { 114 | flag.CommandLine = oldCommandLine 115 | }() 116 | cfg := &cfg1{StringValue1: "value1"} 117 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) 118 | err := ParseToDef(cfg) 119 | assert.NoError(t, err) 120 | err = flag.CommandLine.Parse([]string{"-string-value1", "value2"}) 121 | assert.NoError(t, err) 122 | assert.Equal(t, "value2", cfg.StringValue1) 123 | err = ParseToDef("bad string") 124 | assert.Error(t, err) 125 | } 126 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 2 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 3 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 4 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 5 | github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco= 6 | github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 8 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 13 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 17 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 18 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 19 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 20 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 21 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 24 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 25 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 26 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 27 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 28 | github.com/urfave/cli/v3 v3.0.0-alpha9.3 h1:RfQlgUHMRxDMwEEmGsrHd+mXYJpWpXlcJM8w86cpjGs= 29 | github.com/urfave/cli/v3 v3.0.0-alpha9.3/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= 30 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 31 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 32 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 33 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 34 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 35 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /gen/gkingpin/gkingpin_test.go: -------------------------------------------------------------------------------- 1 | package gkingpin 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/alecthomas/kingpin/v2" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/urfave/sflags" 11 | ) 12 | 13 | type cfg1 struct { 14 | StringValue1 string 15 | StringValue2 string `flag:"string-value-two s"` 16 | StringValue3 string `flag:",required"` 17 | 18 | CounterValue1 sflags.Counter 19 | 20 | StringSliceValue1 []string 21 | } 22 | 23 | func TestParse(t *testing.T) { 24 | tests := []struct { 25 | name string 26 | 27 | cfg interface{} 28 | args []string 29 | expCfg interface{} 30 | expErr1 error // sflag Parse error 31 | expErr2 error // kingpin Parse error 32 | }{ 33 | { 34 | name: "Test cfg1", 35 | cfg: &cfg1{ 36 | StringValue1: "string_value1_value", 37 | StringValue2: "string_value2_value", 38 | StringValue3: "string_value3_value", 39 | 40 | CounterValue1: 1, 41 | 42 | StringSliceValue1: []string{"one", "two"}, 43 | }, 44 | expCfg: &cfg1{ 45 | StringValue1: "string_value1_value2", 46 | StringValue2: "string_value2_value2", 47 | StringValue3: "string_value3_value2", 48 | 49 | CounterValue1: 3, 50 | 51 | StringSliceValue1: []string{ 52 | "one2", "two2", "three", "4"}, 53 | }, 54 | args: []string{ 55 | "--string-value1", "string_value1_value2", 56 | "--string-value-two", "string_value2_value2", 57 | "--string-value3", "string_value3_value2", 58 | "--counter-value1", "--counter-value1", 59 | "--string-slice-value1", "one2", 60 | "--string-slice-value1", "two2", 61 | "--string-slice-value1", "three,4", 62 | }, 63 | }, 64 | { 65 | name: "Test cfg1 no args", 66 | cfg: &cfg1{ 67 | StringValue1: "string_value1_value", 68 | StringValue2: "", 69 | }, 70 | expCfg: &cfg1{ 71 | StringValue1: "string_value1_value", 72 | StringValue2: "", 73 | }, 74 | args: []string{}, 75 | expErr2: errors.New("required flag(s) '--string-value3' not provided"), 76 | }, 77 | { 78 | name: "Test cfg1 short option", 79 | cfg: &cfg1{ 80 | StringValue2: "string_value2_value", 81 | }, 82 | expCfg: &cfg1{ 83 | StringValue2: "string_value2_value2", 84 | StringValue3: "string_value3_value", 85 | }, 86 | args: []string{ 87 | "--string-value3", "string_value3_value", 88 | "-s", "string_value2_value2", 89 | }, 90 | }, 91 | { 92 | name: "Test cfg1 without default values", 93 | cfg: &cfg1{}, 94 | expCfg: &cfg1{ 95 | StringValue1: "string_value1_value2", 96 | StringValue2: "string_value2_value2", 97 | StringValue3: "string_value3_value2", 98 | 99 | CounterValue1: 1, 100 | }, 101 | args: []string{ 102 | "--string-value1", "string_value1_value2", 103 | "--string-value-two", "string_value2_value2", 104 | "--string-value3", "string_value3_value2", 105 | // kingpin can't pass value for boolean arguments. 106 | //"--counter-value1", "2", 107 | "--counter-value1", 108 | }, 109 | }, 110 | { 111 | name: "Test cfg1 bad option", 112 | cfg: &cfg1{ 113 | StringValue1: "string_value1_value", 114 | }, 115 | args: []string{ 116 | "--bad-value=string_value1_value2", 117 | }, 118 | expErr2: errors.New("unknown long flag '--bad-value'"), 119 | }, 120 | { 121 | name: "Test bad cfg value", 122 | cfg: "bad config", 123 | expErr1: errors.New("object must be a pointer to struct or interface"), 124 | }, 125 | } 126 | for _, test := range tests { 127 | t.Run(test.name, func(t *testing.T) { 128 | app := kingpin.New("testApp", "") 129 | app.Terminate(nil) 130 | 131 | err := ParseTo(test.cfg, app) 132 | if test.expErr1 != nil { 133 | require.Error(t, err) 134 | require.Equal(t, test.expErr1, err) 135 | } else { 136 | require.NoError(t, err) 137 | } 138 | if err != nil { 139 | return 140 | } 141 | 142 | _, err = app.Parse(test.args) 143 | if test.expErr2 != nil { 144 | require.Error(t, err) 145 | require.Equal(t, test.expErr2, err) 146 | } else { 147 | require.NoError(t, err) 148 | } 149 | if err != nil { 150 | return 151 | } 152 | assert.Equal(t, test.expCfg, test.cfg) 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /validator/govalidator/govalidator_test.go: -------------------------------------------------------------------------------- 1 | package govalidator 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/urfave/sflags" 12 | "github.com/urfave/sflags/gen/gflag" 13 | ) 14 | 15 | func ExampleNew() { 16 | type config struct { 17 | Host string `valid:"host"` 18 | Port int `valid:"port"` 19 | } 20 | cfg := &config{ 21 | Host: "127.0.0.1", 22 | Port: 6000, 23 | } 24 | // Use gflags.ParseToDef if you want default `flag.CommandLine` 25 | fs, err := gflag.Parse(cfg, sflags.Validator(New())) 26 | if err != nil { 27 | log.Fatalf("err: %v", err) 28 | } 29 | fs.Init("text", flag.ContinueOnError) 30 | fs.SetOutput(io.Discard) 31 | 32 | // if we pass a wrong domain to the host flag, we'll get a error. 33 | if err = fs.Parse([]string{"-host", "wrong domain"}); err != nil { 34 | fmt.Printf("err: %v\n", err) 35 | } 36 | // if we pass a wrong port to the port flag, we'll get a error. 37 | if err = fs.Parse([]string{"-port", "800000"}); err != nil { 38 | fmt.Printf("err: %v\n", err) 39 | } 40 | // Output: 41 | // err: invalid value "wrong domain" for flag -host: `wrong domain` does not validate as host 42 | // err: invalid value "800000" for flag -port: `800000` does not validate as port 43 | } 44 | 45 | func Test_isValidTag(t *testing.T) { 46 | tests := []struct { 47 | arg string 48 | want bool 49 | }{ 50 | {"simple", true}, 51 | {"", false}, 52 | {"!#$%&()*+-./:<=>?@[]^_{|}~ ", true}, 53 | {"абв", true}, 54 | {"`", false}, 55 | } 56 | for _, tt := range tests { 57 | assert.Equal(t, tt.want, isValidTag(tt.arg), "for %v", tt.arg) 58 | } 59 | } 60 | 61 | func Test_parseTagIntoMap(t *testing.T) { 62 | tests := []struct { 63 | tag string 64 | want tagOptionsMap 65 | }{ 66 | { 67 | tag: "required~Some error message,length(2|3)", 68 | want: tagOptionsMap{ 69 | "required": "Some error message", 70 | "length(2|3)": "", 71 | }, 72 | }, 73 | { 74 | tag: "required~Some error message~other", 75 | want: tagOptionsMap{ 76 | "required": "", 77 | }, 78 | }, 79 | { 80 | tag: "bad`tag,good_tag", 81 | want: tagOptionsMap{ 82 | "good_tag": "", 83 | }, 84 | }, 85 | } 86 | for _, tt := range tests { 87 | assert.Equal(t, tt.want, parseTagIntoMap(tt.tag), "for %v", tt.tag) 88 | } 89 | } 90 | 91 | func Test_validateFunc(t *testing.T) { 92 | tests := []struct { 93 | val string 94 | options tagOptionsMap 95 | 96 | expErr string 97 | }{ 98 | { 99 | val: "not a host", 100 | options: tagOptionsMap{"host": ""}, 101 | expErr: "`not a host` does not validate as host", 102 | }, 103 | { 104 | val: "localhost", 105 | options: tagOptionsMap{"host": ""}, 106 | expErr: "", 107 | }, 108 | { 109 | val: "localhost", 110 | options: tagOptionsMap{"!host": ""}, 111 | expErr: "`localhost` does validate as host", 112 | }, 113 | { 114 | val: "not a host", 115 | options: tagOptionsMap{"host": "wrong host value"}, 116 | expErr: "wrong host value", 117 | }, 118 | { 119 | val: "localhost", 120 | options: tagOptionsMap{"!host": "shouldn't be a host"}, 121 | expErr: "shouldn't be a host", 122 | }, 123 | { 124 | val: "localhost", 125 | options: tagOptionsMap{"length(2|10)": ""}, 126 | expErr: "", 127 | }, 128 | { 129 | val: "localhostlong", 130 | options: tagOptionsMap{"length(2|10)": ""}, 131 | expErr: "`localhostlong` does not validate as length(2|10)", 132 | }, 133 | { 134 | val: "localhostlong", 135 | options: tagOptionsMap{"length(2|10)": "too long!"}, 136 | expErr: "too long!", 137 | }, 138 | { 139 | val: "localhost", 140 | options: tagOptionsMap{"!length(2|10)": ""}, 141 | expErr: "`localhost` does validate as length(2|10)", 142 | }, 143 | { 144 | val: "localhost", 145 | options: tagOptionsMap{"!length(2|10)": "should be longer"}, 146 | expErr: "should be longer", 147 | }, 148 | } 149 | for _, tt := range tests { 150 | err := validateFunc(tt.val, tt.options) 151 | if tt.expErr != "" { 152 | if assert.Error(t, err) { 153 | assert.EqualError(t, err, tt.expErr) 154 | } 155 | } else { 156 | assert.NoError(t, err) 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /gen/gcli/gcli_test.go: -------------------------------------------------------------------------------- 1 | package gcli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "github.com/urfave/cli/v2" 13 | "github.com/urfave/sflags" 14 | ) 15 | 16 | type cfg1 struct { 17 | StringValue1 string 18 | StringValue2 string `flag:"string-value-two s"` 19 | StringValue3 string `flag:",required"` 20 | 21 | CounterValue1 sflags.Counter 22 | 23 | StringSliceValue1 []string 24 | } 25 | 26 | func TestParse(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | 30 | cfg interface{} 31 | args []string 32 | expCfg interface{} 33 | expErr1 error // sflag Parse error 34 | expErr2 error // cli Parse error 35 | }{ 36 | { 37 | name: "Test cfg1", 38 | cfg: &cfg1{ 39 | StringValue1: "string_value1_value", 40 | StringValue2: "string_value2_value", 41 | StringValue3: "string_value3_value", 42 | 43 | CounterValue1: 1, 44 | 45 | StringSliceValue1: []string{"one", "two"}, 46 | }, 47 | expCfg: &cfg1{ 48 | StringValue1: "string_value1_value2", 49 | StringValue2: "string_value2_value2", 50 | StringValue3: "string_value3_value2", 51 | 52 | CounterValue1: 3, 53 | 54 | StringSliceValue1: []string{ 55 | "one2", "two2", "three", "4"}, 56 | }, 57 | args: []string{ 58 | "--string-value1", "string_value1_value2", 59 | "--string-value-two", "string_value2_value2", 60 | "--string-value3", "string_value3_value2", 61 | "--counter-value1", "--counter-value1", 62 | "--string-slice-value1", "one2", 63 | "--string-slice-value1", "two2", 64 | "--string-slice-value1", "three,4", 65 | }, 66 | }, 67 | { 68 | name: "Test cfg1 no args", 69 | cfg: &cfg1{ 70 | StringValue1: "string_value1_value", 71 | StringValue2: "", 72 | }, 73 | expCfg: &cfg1{ 74 | StringValue1: "string_value1_value", 75 | StringValue2: "", 76 | }, 77 | args: []string{}, 78 | expErr2: fmt.Errorf("required flag \"string-value3\" not set"), 79 | }, 80 | { 81 | name: "Test cfg1 short option", 82 | cfg: &cfg1{ 83 | StringValue2: "string_value2_value", 84 | }, 85 | expCfg: &cfg1{ 86 | StringValue2: "string_value2_value2", 87 | StringValue3: "string_value3_value2", 88 | }, 89 | args: []string{ 90 | "--string-value3", "string_value3_value2", 91 | "-s=string_value2_value2", 92 | }, 93 | }, 94 | { 95 | name: "Test cfg1 without default values", 96 | cfg: &cfg1{}, 97 | expCfg: &cfg1{ 98 | StringValue1: "string_value1_value2", 99 | StringValue2: "string_value2_value2", 100 | StringValue3: "string_value3_value2", 101 | 102 | CounterValue1: 3, 103 | }, 104 | args: []string{ 105 | "--string-value1", "string_value1_value2", 106 | "--string-value-two", "string_value2_value2", 107 | "--string-value3", "string_value3_value2", 108 | "--counter-value1=2", "--counter-value1", 109 | }, 110 | }, 111 | { 112 | name: "Test cfg1 bad option", 113 | cfg: &cfg1{ 114 | StringValue1: "string_value1_value", 115 | }, 116 | args: []string{ 117 | "--bad-value=string_value1_value2", 118 | }, 119 | expErr2: errors.New("flag provided but not defined: -bad-value"), 120 | }, 121 | { 122 | name: "Test bad cfg value", 123 | cfg: "bad config", 124 | expErr1: errors.New("object must be a pointer to struct or interface"), 125 | }, 126 | } 127 | // forbid urfave/cli to exit 128 | cli.OsExiter = func(i int) {} 129 | for _, test := range tests { 130 | t.Run(test.name, func(t *testing.T) { 131 | flags, err := Parse(test.cfg) 132 | if test.expErr1 != nil { 133 | require.Error(t, err) 134 | require.Equal(t, test.expErr1, err) 135 | } else { 136 | require.NoError(t, err) 137 | } 138 | if err != nil { 139 | return 140 | } 141 | cliApp := cli.NewApp() 142 | cliApp.Action = func(c *cli.Context) error { 143 | return nil 144 | } 145 | cliApp.UseShortOptionHandling = true 146 | cli.ErrWriter = io.Discard 147 | cliApp.OnUsageError = func(_ *cli.Context, err error, _ bool) error { 148 | return err 149 | } 150 | 151 | cliApp.Flags = flags 152 | args := append([]string{"cliApp"}, test.args...) 153 | err = cliApp.Run(args) 154 | if test.expErr2 != nil { 155 | require.Error(t, err) 156 | require.Equal(t, test.expErr2.Error(), strings.ToLower(err.Error())) 157 | } else { 158 | require.NoError(t, err) 159 | } 160 | if err != nil { 161 | return 162 | } 163 | assert.Equal(t, test.expCfg, test.cfg) 164 | }) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /camelcase.go: -------------------------------------------------------------------------------- 1 | package sflags 2 | 3 | // This part was taken from the https://github.com/fatih/camelcase package. 4 | // The only fix is for integers, they stay with previous words. 5 | // 6 | // The MIT License (MIT) 7 | // 8 | // Copyright (c) 2015 Fatih Arslan 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 11 | // this software and associated documentation files (the "Software"), to deal in 12 | // the Software without restriction, including without limitation the rights to 13 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 14 | // the Software, and to permit persons to whom the Software is furnished to do so, 15 | // subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 22 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 23 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 24 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | import ( 28 | "unicode" 29 | "unicode/utf8" 30 | ) 31 | 32 | const ( 33 | classLower = 1 34 | classUpper = 2 35 | classDigit = 3 36 | classOther = 4 37 | ) 38 | 39 | // split splits the camelcase word and returns a list of words. It also 40 | // supports digits. Both lower camel case and upper camel case are supported. 41 | // For more info please check: http://en.wikipedia.org/wiki/CamelCase 42 | // 43 | // Examples 44 | // 45 | // "" => [""] 46 | // "lowercase" => ["lowercase"] 47 | // "Class" => ["Class"] 48 | // "MyClass" => ["My", "Class"] 49 | // "MyC" => ["My", "C"] 50 | // "HTML" => ["HTML"] 51 | // "PDFLoader" => ["PDF", "Loader"] 52 | // "AString" => ["A", "String"] 53 | // "SimpleXMLParser" => ["Simple", "XML", "Parser"] 54 | // "vimRPCPlugin" => ["vim", "RPC", "Plugin"] 55 | // "GL11Version" => ["GL11", "Version"] 56 | // "99Bottles" => ["99", "Bottles"] 57 | // "May5" => ["May5"] 58 | // "BFG9000" => ["BFG9000"] 59 | // "BöseÜberraschung" => ["Böse", "Überraschung"] 60 | // "Two spaces" => ["Two", " ", "spaces"] 61 | // "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] 62 | // 63 | // Splitting rules 64 | // 65 | // 1. If string is not valid UTF-8, return it without splitting as 66 | // single item array. 67 | // 2. Assign all unicode characters into one of 4 sets: lower case 68 | // letters, upper case letters, numbers, and all other characters. 69 | // 3. Iterate through characters of string, introducing splits 70 | // between adjacent characters that belong to different sets. 71 | // 4. Iterate through array of split strings, and if a given string 72 | // is upper case: 73 | // if subsequent string is lower case: 74 | // move last character of upper case string to beginning of 75 | // lower case string 76 | func split(src string) (entries []string) { 77 | // don't split invalid utf8 78 | if !utf8.ValidString(src) { 79 | return []string{src} 80 | } 81 | entries = []string{} 82 | var runes [][]rune 83 | var class int 84 | lastClass := 0 85 | // split into fields based on class of unicode character 86 | for _, r := range src { 87 | switch true { 88 | case unicode.IsLower(r): 89 | class = classLower 90 | case unicode.IsUpper(r): 91 | class = classUpper 92 | case unicode.IsDigit(r): 93 | class = classDigit 94 | default: 95 | class = classOther 96 | } 97 | if lastClass != 0 && (class == lastClass || class == classDigit) { 98 | runes[len(runes)-1] = append(runes[len(runes)-1], r) 99 | } else { 100 | runes = append(runes, []rune{r}) 101 | } 102 | lastClass = class 103 | } 104 | // handle upper case -> lower case sequences, e.g. 105 | // "PDFL", "oader" -> "PDF", "Loader" 106 | for i := 0; i < len(runes)-1; i++ { 107 | if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) { 108 | runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...) 109 | runes[i] = runes[i][:len(runes[i])-1] 110 | } 111 | } 112 | // construct []string from results 113 | for _, s := range runes { 114 | if len(s) > 0 { 115 | entries = append(entries, string(s)) 116 | } 117 | } 118 | return 119 | } 120 | -------------------------------------------------------------------------------- /gen/gcli/gcliv3_test.go: -------------------------------------------------------------------------------- 1 | package gcli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | "github.com/urfave/cli/v3" 14 | "github.com/urfave/sflags" 15 | ) 16 | 17 | type cfg2 struct { 18 | StringValue1 string 19 | StringValue2 string `flag:"string-value-two s"` 20 | StringValue3 string `flag:",required"` 21 | 22 | CounterValue1 sflags.Counter 23 | 24 | StringSliceValue1 []string 25 | } 26 | 27 | func TestParseV3(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | 31 | cfg interface{} 32 | args []string 33 | expCfg interface{} 34 | expErr1 error // sflag Parse error 35 | expErr2 error // cli Parse error 36 | }{ 37 | { 38 | name: "Test cfg2", 39 | cfg: &cfg2{ 40 | StringValue1: "string_value1_value", 41 | StringValue2: "string_value2_value", 42 | StringValue3: "string_value3_value", 43 | 44 | CounterValue1: 1, 45 | 46 | StringSliceValue1: []string{"one", "two"}, 47 | }, 48 | expCfg: &cfg2{ 49 | StringValue1: "string_value1_value2", 50 | StringValue2: "string_value2_value2", 51 | StringValue3: "string_value3_value2", 52 | 53 | CounterValue1: 3, 54 | 55 | StringSliceValue1: []string{ 56 | "one2", "two2", "three", "4"}, 57 | }, 58 | args: []string{ 59 | "--string-value1", "string_value1_value2", 60 | "--string-value-two", "string_value2_value2", 61 | "--string-value3", "string_value3_value2", 62 | "--counter-value1", "--counter-value1", 63 | "--string-slice-value1", "one2", 64 | "--string-slice-value1", "two2", 65 | "--string-slice-value1", "three,4", 66 | }, 67 | }, 68 | { 69 | name: "Test cfg2 no args", 70 | cfg: &cfg2{ 71 | StringValue1: "string_value1_value", 72 | StringValue2: "", 73 | }, 74 | expCfg: &cfg2{ 75 | StringValue1: "string_value1_value", 76 | StringValue2: "", 77 | }, 78 | args: []string{}, 79 | expErr2: fmt.Errorf("required flag \"string-value3\" not set"), 80 | }, 81 | { 82 | name: "Test cfg2 short option", 83 | cfg: &cfg2{ 84 | StringValue2: "string_value2_value", 85 | }, 86 | expCfg: &cfg2{ 87 | StringValue2: "string_value2_value2", 88 | StringValue3: "string_value3_value2", 89 | }, 90 | args: []string{ 91 | "--string-value3", "string_value3_value2", 92 | "-s=string_value2_value2", 93 | }, 94 | }, 95 | { 96 | name: "Test cfg2 without default values", 97 | cfg: &cfg2{}, 98 | expCfg: &cfg2{ 99 | StringValue1: "string_value1_value2", 100 | StringValue2: "string_value2_value2", 101 | StringValue3: "string_value3_value2", 102 | 103 | CounterValue1: 3, 104 | }, 105 | args: []string{ 106 | "--string-value1", "string_value1_value2", 107 | "--string-value-two", "string_value2_value2", 108 | "--string-value3", "string_value3_value2", 109 | "--counter-value1=2", "--counter-value1", 110 | }, 111 | }, 112 | { 113 | name: "Test cfg2 bad option", 114 | cfg: &cfg2{ 115 | StringValue1: "string_value1_value", 116 | }, 117 | args: []string{ 118 | "--bad-value=string_value1_value2", 119 | }, 120 | expErr2: errors.New("flag provided but not defined: -bad-value"), 121 | }, 122 | { 123 | name: "Test bad cfg value", 124 | cfg: "bad config", 125 | expErr1: errors.New("object must be a pointer to struct or interface"), 126 | }, 127 | } 128 | // forbid urfave/cli to exit 129 | cli.OsExiter = func(i int) {} 130 | for _, test := range tests { 131 | t.Run(test.name, func(t *testing.T) { 132 | flags, err := ParseV3(test.cfg) 133 | if test.expErr1 != nil { 134 | require.Error(t, err) 135 | require.Equal(t, test.expErr1, err) 136 | } else { 137 | require.NoError(t, err) 138 | } 139 | if err != nil { 140 | return 141 | } 142 | cmd := &cli.Command{} 143 | cmd.Action = func(_ context.Context, c *cli.Command) error { 144 | return nil 145 | } 146 | cmd.UseShortOptionHandling = true 147 | cli.ErrWriter = io.Discard 148 | cmd.OnUsageError = func(ctx context.Context, cmd *cli.Command, err error, isSubcommand bool) error { 149 | return err 150 | } 151 | 152 | cmd.Flags = flags 153 | args := append([]string{"cliApp"}, test.args...) 154 | err = cmd.Run(context.Background(), args) 155 | if test.expErr2 != nil { 156 | require.Error(t, err) 157 | require.Equal(t, test.expErr2.Error(), strings.ToLower(err.Error())) 158 | } else { 159 | require.NoError(t, err) 160 | } 161 | if err != nil { 162 | return 163 | } 164 | assert.Equal(t, test.expCfg, test.cfg) 165 | }) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /values.go: -------------------------------------------------------------------------------- 1 | package sflags 2 | 3 | //go:generate go run ./cmd/genvalues/main.go 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Value is the interface to the dynamic value stored in v flag. 13 | // (The default value is represented as v string.) 14 | // 15 | // If v Value has an IsBoolFlag() bool method returning true, the command-line 16 | // parser makes --name equivalent to -name=true rather than using the next 17 | // command-line argument, and adds v --no-name counterpart for negating the 18 | // flag. 19 | type Value interface { 20 | String() string 21 | Set(string) error 22 | 23 | // pflag.Flag require this 24 | Type() string 25 | } 26 | 27 | // Getter is an interface that allows the contents of v Value to be retrieved. 28 | // It wraps the Value interface, rather than being part of it, because it 29 | // appeared after Go 1 and its compatibility rules. All Value types provided 30 | // by this package satisfy the Getter interface. 31 | type Getter interface { 32 | Value 33 | Get() interface{} 34 | } 35 | 36 | // BoolFlag is an optional interface to indicate boolean flags 37 | // that don't accept v value, and implicitly have v --no- negation counterpart. 38 | type BoolFlag interface { 39 | Value 40 | IsBoolFlag() bool 41 | } 42 | 43 | // RepeatableFlag is an optional interface for flags that can be repeated. 44 | // required by kingpin 45 | type RepeatableFlag interface { 46 | Value 47 | IsCumulative() bool 48 | } 49 | 50 | // === Custom values 51 | 52 | type validateValue struct { 53 | Value 54 | validateFunc func(val string) error 55 | } 56 | 57 | func (v *validateValue) IsBoolFlag() bool { 58 | if boolFlag, casted := v.Value.(BoolFlag); casted { 59 | return boolFlag.IsBoolFlag() 60 | } 61 | return false 62 | } 63 | 64 | func (v *validateValue) IsCumulative() bool { 65 | if cumulativeFlag, casted := v.Value.(RepeatableFlag); casted { 66 | return cumulativeFlag.IsCumulative() 67 | } 68 | return false 69 | } 70 | 71 | func (v *validateValue) String() string { 72 | if v == nil || v.Value == nil { 73 | return "" 74 | } 75 | return v.Value.String() 76 | } 77 | 78 | func (v *validateValue) Set(val string) error { 79 | if v.validateFunc != nil { 80 | err := v.validateFunc(val) 81 | if err != nil { 82 | return err 83 | } 84 | } 85 | return v.Value.Set(val) 86 | } 87 | 88 | // HexBytes might be used if you want to parse slice of bytes as hex string. 89 | // Original `[]byte` or `[]uint8` parsed as a list of `uint8`. 90 | type HexBytes []byte 91 | 92 | // Counter type is useful if you want to save number 93 | // by using flag multiple times in command line. 94 | // It's a boolean type, so you can use it without value. 95 | // If you use `struct{count Counter} 96 | // and parse it with `-count=10 ... -count .. -count`, 97 | // then final value of `count` will be 12. 98 | // Implements Value, Getter, BoolFlag, RepeatableFlag interfaces 99 | type Counter int 100 | 101 | var _ RepeatableFlag = (*Counter)(nil) 102 | 103 | // Set method parses string from command line. 104 | func (v *Counter) Set(s string) error { 105 | // flag package pass true if BoolFlag doesn't have an argument. 106 | if s == "" || s == "true" { 107 | *v++ 108 | return nil 109 | } 110 | parsed, err := strconv.ParseInt(s, 0, 0) 111 | if err != nil { 112 | return err 113 | } 114 | // -1 means that no specific value was passed, so increment 115 | if parsed == -1 { 116 | *v++ 117 | } else { 118 | *v = Counter(parsed) 119 | } 120 | return nil 121 | } 122 | 123 | // Get method returns inner value for Counter. 124 | func (v Counter) Get() interface{} { return int(v) } 125 | 126 | // IsBoolFlag returns true, because Counter might be used without value. 127 | func (v Counter) IsBoolFlag() bool { return true } 128 | 129 | // String returns string representation of Counter 130 | func (v Counter) String() string { return strconv.Itoa(int(v)) } 131 | 132 | // IsCumulative returns true, because Counter might be used multiple times. 133 | func (v Counter) IsCumulative() bool { return true } 134 | 135 | // Type returns `count` for Counter, it's mostly for pflag compatibility. 136 | func (v Counter) Type() string { return "count" } 137 | 138 | // === Some patches for generated flags 139 | 140 | // IsBoolFlag returns true. boolValue implements BoolFlag interface. 141 | func (v *boolValue) IsBoolFlag() bool { return true } 142 | 143 | // === Custom parsers 144 | 145 | func parseIP(s string) (net.IP, error) { 146 | ip := net.ParseIP(strings.TrimSpace(s)) 147 | if ip == nil { 148 | return nil, fmt.Errorf("failed to parse IP: %q", s) 149 | } 150 | return ip, nil 151 | } 152 | 153 | func parseTCPAddr(s string) (net.TCPAddr, error) { 154 | tcpADDR, err := net.ResolveTCPAddr("tcp", strings.TrimSpace(s)) 155 | if err != nil { 156 | return net.TCPAddr{}, fmt.Errorf("failed to parse TCPAddr: %q", s) 157 | } 158 | return *tcpADDR, nil 159 | } 160 | 161 | func parseIPNet(s string) (net.IPNet, error) { 162 | _, ipNet, err := net.ParseCIDR(s) 163 | if err != nil { 164 | return net.IPNet{}, err 165 | } 166 | return *ipNet, nil 167 | } 168 | -------------------------------------------------------------------------------- /validator/govalidator/govalidator.go: -------------------------------------------------------------------------------- 1 | // Package govalidator adds support for govalidator library. 2 | // Part of this package was taken from govalidator private api 3 | // and covered by MIT license. 4 | // 5 | // The MIT License (MIT) 6 | // 7 | // # Copyright (c) 2014 Alex Saskevich 8 | // 9 | // Permission is hereby granted, free of charge, to any person obtaining a copy 10 | // of this software and associated documentation files (the "Software"), to deal 11 | // in the Software without restriction, including without limitation the rights 12 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | // copies of the Software, and to permit persons to whom the Software is 14 | // furnished to do so, subject to the following conditions: 15 | // 16 | // The above copyright notice and this permission notice shall be included in all 17 | // copies or substantial portions of the Software. 18 | // 19 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | // SOFTWARE. 26 | package govalidator 27 | 28 | import ( 29 | "fmt" 30 | "reflect" 31 | "strings" 32 | "unicode" 33 | 34 | "github.com/asaskevich/govalidator" 35 | ) 36 | 37 | const ( 38 | validTag = "valid" // tag isn't optional in govalidator 39 | ) 40 | 41 | type tagOptionsMap map[string]string 42 | 43 | func isValidTag(s string) bool { 44 | if s == "" { 45 | return false 46 | } 47 | for _, c := range s { 48 | switch { 49 | case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c): 50 | // Backslash and quote chars are reserved, but 51 | // otherwise any punctuation chars are allowed 52 | // in a tag name. 53 | default: 54 | if !unicode.IsLetter(c) && !unicode.IsDigit(c) { 55 | return false 56 | } 57 | } 58 | } 59 | return true 60 | } 61 | 62 | // parseTagIntoMap parses a struct tag `valid:"required~Some error message,length(2|3)"` into map[string]string{"required": "Some error message", "length(2|3)": ""} 63 | func parseTagIntoMap(tag string) tagOptionsMap { 64 | optionsMap := make(tagOptionsMap) 65 | options := strings.SplitN(tag, ",", -1) 66 | for _, option := range options { 67 | validationOptions := strings.Split(option, "~") 68 | if !isValidTag(validationOptions[0]) { 69 | continue 70 | } 71 | if len(validationOptions) == 2 { 72 | optionsMap[validationOptions[0]] = validationOptions[1] 73 | } else { 74 | optionsMap[validationOptions[0]] = "" 75 | } 76 | } 77 | return optionsMap 78 | } 79 | 80 | func validateFunc(val string, options tagOptionsMap) error { 81 | 82 | // for each tag option check the map of validator functions 83 | for validator, customErrorMessage := range options { 84 | var negate bool 85 | customMsgExists := len(customErrorMessage) > 0 86 | // Check wether the tag looks like '!something' or 'something' 87 | if validator[0] == '!' { 88 | validator = string(validator[1:]) 89 | negate = true 90 | } 91 | // Check for param validators 92 | for key, value := range govalidator.ParamTagRegexMap { 93 | ps := value.FindStringSubmatch(validator) 94 | if len(ps) > 0 { 95 | if validatefunc, ok := govalidator.ParamTagMap[key]; ok { 96 | 97 | if result := validatefunc(val, ps[1:]...); (!result && !negate) || (result && negate) { 98 | var err error 99 | if !negate { 100 | if customMsgExists { 101 | err = fmt.Errorf("%s", customErrorMessage) 102 | } else { 103 | err = fmt.Errorf("`%s` does not validate as %s", val, validator) 104 | } 105 | 106 | } else { 107 | if customMsgExists { 108 | err = fmt.Errorf("%s", customErrorMessage) 109 | } else { 110 | err = fmt.Errorf("`%s` does validate as %s", val, validator) 111 | } 112 | } 113 | return err 114 | } 115 | 116 | } 117 | } 118 | } 119 | 120 | if validatefunc, ok := govalidator.TagMap[validator]; ok { 121 | if result := validatefunc(val); !result && !negate || result && negate { 122 | var err error 123 | 124 | if !negate { 125 | if customMsgExists { 126 | err = fmt.Errorf("%s", customErrorMessage) 127 | } else { 128 | err = fmt.Errorf("`%s` does not validate as %s", val, validator) 129 | } 130 | } else { 131 | if customMsgExists { 132 | err = fmt.Errorf("%s", customErrorMessage) 133 | } else { 134 | err = fmt.Errorf("`%s` does validate as %s", val, validator) 135 | } 136 | } 137 | return err 138 | } 139 | 140 | } 141 | } 142 | return nil 143 | } 144 | 145 | // New returns ValidateFunc for govalidator library. 146 | // Supports default String validators in TagMap and ParamTagMap. 147 | // Doesn't support custom type validators and required filters. 148 | // Please check all available functions at https://github.com/asaskevich/govalidator. 149 | func New() func(val string, field reflect.StructField, obj interface{}) error { 150 | return func(val string, field reflect.StructField, obj interface{}) error { 151 | options := parseTagIntoMap(field.Tag.Get(validTag)) 152 | return validateFunc(val, options) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flags based on structures [![GoDoc](https://godoc.org/github.com/urfave/sflags?status.svg)](http://godoc.org/github.com/urfave/sflags) [![Build Status](https://img.shields.io/github/check-runs/urfave/sflags/main?label=build%20status)](https://github.com/urfave/sflags/actions?query=branch%3Amain) [![codecov](https://codecov.io/gh/urfave/sflags/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/sflags) [![Go Report Card](https://goreportcard.com/badge/github.com/urfave/sflags)](https://goreportcard.com/report/github.com/urfave/sflags) 2 | 3 | The sflags package uses structs, reflection and struct field tags 4 | to allow you specify command line options. It supports [different types](#supported-types-in-structures) and [features](#features). 5 | 6 | An example: 7 | 8 | ```golang 9 | type HTTPConfig struct { 10 | Host string `desc:"HTTP host"` 11 | Port int `flag:"port p" desc:"some port"` 12 | SSL bool `env:"HTTP_SSL_VALUE"` 13 | Timeout time.Duration `flag:",deprecated,hidden"` 14 | } 15 | 16 | type Config struct { 17 | HTTP HTTPConfig 18 | Stats StatsConfig 19 | } 20 | ``` 21 | 22 | And you can use your favorite flag or cli library! 23 | 24 | ## Supported libraries and features: 25 | 26 | | | | Hidden | Deprecated | Short | Env | Required | 27 | | --- | --- |:------:|:----------:|:-----:|:---:|:--------:| 28 | |