├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── commands.go ├── completer.go ├── demo ├── flagly-binding │ └── flagly-binding.go ├── flagly-git │ ├── flagly-git.go │ ├── git-add.go │ ├── git-clone.go │ └── git-init.go └── flagly-shell │ └── flagly-shell.go ├── flagly.go ├── go.mod ├── go.sum ├── handler.go ├── iface.go ├── option.go ├── tagparser.go ├── tagparser_test.go ├── typer.go ├── typer_test.go └── usage.go /.gitignore: -------------------------------------------------------------------------------- 1 | demo/flagly-gitd 2 | demo/flagly-supervisorctl 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.5 4 | env: 5 | - GOBIN=~ 6 | script: 7 | - go install github.com/chzyer/flagly/... 8 | - go test -v 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 go-flagly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flagly 2 | 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) 4 | [![Build Status](https://travis-ci.org/chzyer/flagly.svg?branch=master)](https://travis-ci.org/chzyer/flagly) 5 | [![GoDoc](https://godoc.org/github.com/chzyer/flagly?status.svg)](https://godoc.org/github.com/chzyer/flagly) 6 | [![codebeat badge](https://codebeat.co/badges/4efbd40b-4d84-48f5-8363-df06c2e9b241)](https://codebeat.co/projects/github-com-chzyer-flagly) 7 | 8 | The easier way to parsing command-line flag in Golang, also building a command line app. 9 | 10 | It can also provides shell-like interactives by using [readline](https://github.com/chzyer/readline) (demo: [flagly-shell](https://github.com/chzyer/flagly/blob/master/demo/flagly-shell/flagly-shell.go)) 11 | 12 | # Usage 13 | 14 | ``` 15 | go get github.com/chzyer/flagly 16 | ``` 17 | 18 | ## binding 19 | 20 | ```{go} 21 | type Config struct { 22 | Verbose bool `name:"v" desc:"be more verbose"` 23 | Name string `type:"[0]"` 24 | } 25 | 26 | func NewConfig() *Config { 27 | var cfg Config 28 | flagly.Bind(&cfg) 29 | return &cfg 30 | } 31 | 32 | func main() { 33 | cfg := NewConfig() 34 | fmt.Printf("config: %+v\n", cfg) 35 | } 36 | ``` 37 | 38 | source file: [flagly-binding](https://github.com/chzyer/flagly/blob/master/demo/flagly-binding/flagly-binding.go) 39 | 40 | ``` 41 | $ go install github.com/chzyer/flagly/demo/flagly-binding 42 | $ flagly-binding -v name 43 | config: &{Verbose:true Name:name} 44 | ``` 45 | 46 | ## routing 47 | 48 | ```{go} 49 | type Git struct { 50 | Version bool `name:"v" desc:"show version"` 51 | 52 | // sub handlers 53 | Clone *GitClone `flagly:"handler"` 54 | Init *GitInit `flagly:"handler"` 55 | } 56 | 57 | type GitClone struct { 58 | Parent *Git `flagly:"parent"` 59 | 60 | Verbose bool `name:"v" desc:"be more verbose"` 61 | Quiet bool `name:"q" desc:"be more quiet"` 62 | Progress bool `name:"progress" desc:"force progress reporting"` 63 | Template string `arg:"template-directory"` 64 | 65 | Repo string `name:"[0]"` 66 | Dir string `name:"[1]" default:""` 67 | } 68 | 69 | func (g *GitClone) FlaglyHandle() error { 70 | if g.Repo == "" { 71 | return flagly.ErrShowUsage 72 | } 73 | fmt.Printf("git clone %+v %+v\n", g.Parent, g) 74 | return nil 75 | } 76 | 77 | func (g *GitClone) FlaglyDesc() string { 78 | return "Create an empty Git repository or reinitialize an existing one" 79 | } 80 | 81 | type GitInit struct { 82 | Quiet bool `name:"q" desc:"be quiet"` 83 | } 84 | 85 | func (g *GitInit) FlaglyDesc() string { 86 | return "Clone a repository into a new directory" 87 | } 88 | 89 | func main() { 90 | var git Git 91 | flagly.Run(&git) 92 | } 93 | ``` 94 | 95 | source file: [flagly-git](https://github.com/chzyer/flagly/blob/master/demo/flagly-git/flagly-git.go) 96 | 97 | ```{shell} 98 | $ go install github.com/chzyer/flagly/demo/flagly-git 99 | $ flagly-git 100 | 101 | usage: flagly-git [option] 102 | 103 | options: 104 | -v show version 105 | -h show help 106 | 107 | commands: 108 | clone Create an empty Git repository or reinitialize an existing one 109 | init Clone a repository into a new directory 110 | 111 | $ flagly-get -v clone -h 112 | 113 | usage: flagly-git [flagly-git option] clone [option] [--] [] 114 | 115 | options: 116 | -v be more verbose 117 | -q be more quiet 118 | -progress force progress reporting 119 | -template 120 | directory from which templates will be used 121 | -h show help 122 | 123 | flagly-git options: 124 | -v show version 125 | -h show help 126 | 127 | $ flagly-git -v clone repoName 128 | 129 | git clone 130 | &{Version:true Clone: Init: Add:} 131 | &{Parent:0xc20801e220 Verbose:false Quiet:false Progress:false Template: Repo:repoName Dir:} 132 | ``` 133 | 134 | # Feedback 135 | 136 | If you have any questions, please submit a github issue and any pull requests is welcomed :) 137 | 138 | * [https://twitter.com/chzyer](https://twitter.com/chzyer) 139 | * [http://weibo.com/2145262190](http://weibo.com/2145262190) 140 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type CmdHelp struct{} 8 | 9 | func NewHandlerHelp() *Handler { 10 | h := NewHandler("help") 11 | h.CompileIface(&CmdHelp{}) 12 | return h 13 | } 14 | 15 | func (CmdHelp) FlaglyHandle(h *Handler) error { 16 | return errors.New(h.GetRoot().Usage("")) 17 | } 18 | 19 | func (CmdHelp) FlaglyDesc() string { 20 | return "show help" 21 | } 22 | -------------------------------------------------------------------------------- /completer.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | type Tree interface { 4 | GetName() string 5 | GetTreeChildren() []Tree 6 | } 7 | 8 | type HandlerCompleter struct { 9 | h Tree 10 | } 11 | 12 | func (hc *HandlerCompleter) DoSegment(seg [][]rune, n int) [][]rune { 13 | h := hc.h 14 | main: 15 | for level := 0; level < len(seg)-1; { 16 | name := string(seg[level]) 17 | children := h.GetTreeChildren() 18 | for _, child := range children { 19 | if child.GetName() == name { 20 | h = child 21 | level++ 22 | continue main 23 | } 24 | } 25 | h = nil 26 | break 27 | } 28 | if h == nil { 29 | return nil 30 | } 31 | children := h.GetTreeChildren() 32 | ret := make([][]rune, len(children)) 33 | for idx, child := range children { 34 | ret[idx] = []rune(child.GetName()) 35 | } 36 | 37 | return ret 38 | } 39 | 40 | type stringTree struct { 41 | name string 42 | } 43 | 44 | func StringTree(name string) Tree { 45 | return stringTree{name} 46 | } 47 | 48 | func (n stringTree) GetName() string { 49 | return n.name 50 | } 51 | 52 | func (n stringTree) GetTreeChildren() []Tree { 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /demo/flagly-binding/flagly-binding.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/chzyer/flagly" 7 | ) 8 | 9 | type Config struct { 10 | Verbose bool `name:"v" desc:"be more verbose"` 11 | Name string `type:"[0]"` 12 | } 13 | 14 | func NewConfig() *Config { 15 | var cfg Config 16 | flagly.Bind(&cfg) 17 | return &cfg 18 | } 19 | 20 | func main() { 21 | cfg := NewConfig() 22 | fmt.Printf("config: %+v\n", cfg) 23 | } 24 | -------------------------------------------------------------------------------- /demo/flagly-git/flagly-git.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/chzyer/flagly" 4 | 5 | type Git struct { 6 | // flags for flagly-git 7 | Version bool `name:"v"` 8 | 9 | // sub handlers must be specified `flaglyHandler` tag 10 | Clone *GitClone `flagly:"handler"` 11 | Init *GitInit `flagly:"handler"` 12 | 13 | Add *GitAdd `flagly:"handler"` 14 | } 15 | 16 | func (g *Git) FlaglyInit() { 17 | // we can set the description via `flagly.SetDesc` or just a field tag `desc:"xxx"` 18 | flagly.SetDesc(&g.Version, "show version") 19 | } 20 | 21 | func main() { 22 | var git Git 23 | flagly.Run(&git) 24 | } 25 | -------------------------------------------------------------------------------- /demo/flagly-git/git-add.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/chzyer/flagly" 4 | 5 | type GitAdd struct { 6 | Git *Git `flagly:"parent"` 7 | Update bool `name:"u" desc:"update tracked files"` 8 | Interactive bool `name:"i" desc:"interactive picking"` 9 | 10 | PathSpec []string `type:"[]" name:"pathspec"` 11 | } 12 | 13 | func (a *GitAdd) FlaglyDesc() string { 14 | return "Add file contents to the index" 15 | } 16 | 17 | func (a *GitAdd) FlaglyHandle() error { 18 | println("git add") 19 | return flagly.ErrShowUsage 20 | } 21 | -------------------------------------------------------------------------------- /demo/flagly-git/git-clone.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/chzyer/flagly" 7 | ) 8 | 9 | type GitClone struct { 10 | // we can get struct `Git` directly by presenting `flaglyParent`, 11 | Parent *Git `flagly:"parent"` 12 | 13 | Verbose bool `name:"v" desc:"be more verbose"` 14 | Quiet bool `name:"q" desc:"be more quiet"` 15 | Progress bool `name:"progress" desc:"force progress reporting"` 16 | Template string `arg:"template-directory"` 17 | 18 | Repo string `type:"[0]"` 19 | Dir string `type:"[1]" default:"."` 20 | } 21 | 22 | func (g *GitClone) FlaglyInit() { 23 | flagly.SetDesc(&g.Template, "directory from which templates will be used") 24 | } 25 | 26 | func (g *GitClone) FlaglyHandle() error { 27 | if g.Repo == "" { 28 | return flagly.Error("error: repo is empty") 29 | } 30 | 31 | fmt.Printf("git clone\n %+v\n %+v\n", g.Parent, g) 32 | return nil 33 | } 34 | 35 | func (g *GitClone) FlaglyDesc() string { 36 | return "Create an empty Git repository or reinitialize an existing one" 37 | } 38 | -------------------------------------------------------------------------------- /demo/flagly-git/git-init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type GitInit struct { 4 | Quiet bool `name:"q" desc:"be quiet"` 5 | } 6 | 7 | func (GitInit) FlaglyDesc() string { 8 | return "Clone a repository into a new directory" 9 | } 10 | -------------------------------------------------------------------------------- /demo/flagly-shell/flagly-shell.go: -------------------------------------------------------------------------------- 1 | /* 2 | usage: 3 | $ go install github.com/chzyer/flagly/demo/flagly-shell 4 | $ flagly-shell 5 | > help 6 | commands: 7 | help 8 | time 9 | base64 10 | > time 11 | 2016-02-11 12:12:43 12 | > time -h 13 | usage: time [option] [--] [] 14 | 15 | options: 16 | -h show help 17 | > base64 18 | missing content 19 | 20 | usage: base64 [option] [--] 21 | 22 | options: 23 | -d decode string 24 | -h show help 25 | > base64 hello 26 | aGVsbG8= 27 | */ 28 | package main 29 | 30 | import ( 31 | "encoding/base64" 32 | "errors" 33 | "os" 34 | "time" 35 | 36 | "github.com/chzyer/flagly" 37 | "github.com/chzyer/readline" 38 | "github.com/google/shlex" 39 | ) 40 | 41 | type Help struct{} 42 | 43 | func (Help) FlaglyHandle(h *flagly.Handler) error { 44 | return errors.New(h.Parent.Usage("")) 45 | } 46 | 47 | // ----------------------------------------------------------------------------- 48 | 49 | type Time struct { 50 | Layout string `type:"[0]" default:"2006-01-02 15:04:05"` 51 | } 52 | 53 | func (t *Time) FlaglyHandle() error { 54 | now := time.Now() 55 | println(now.Format(t.Layout)) 56 | return nil 57 | } 58 | 59 | type Base64 struct { 60 | IsDecode bool `name:"d" desc:"decode string"` 61 | Content string `type:"[0]"` 62 | } 63 | 64 | func (b *Base64) FlaglyHandle() error { 65 | if b.Content == "" { 66 | return flagly.Error("missing content") 67 | } 68 | if b.IsDecode { 69 | ret, err := base64.URLEncoding.DecodeString(b.Content) 70 | if err != nil { 71 | return err 72 | } 73 | println(string(ret)) 74 | } else { 75 | ret := base64.URLEncoding.EncodeToString([]byte(b.Content)) 76 | println(ret) 77 | } 78 | return nil 79 | } 80 | 81 | // ----------------------------------------------------------------------------- 82 | 83 | type Program struct { 84 | Help *Help `flagly:"handler"` 85 | Time *Time `flagly:"handler"` 86 | Base64 *Base64 `flagly:"handler"` 87 | Level1 *Level1 `flagly:"handler"` 88 | } 89 | 90 | type Level1 struct { 91 | Level2 *Level2 `flagly:"handler"` 92 | } 93 | 94 | type Level2 struct { 95 | Level3 *Level3 `flagly:"handler"` 96 | } 97 | 98 | type Level3 struct{} 99 | 100 | func main() { 101 | rl, err := readline.NewEx(&readline.Config{ 102 | Prompt: "> ", 103 | HistoryFile: "/tmp/flagly-shell.readline", 104 | }) 105 | if err != nil { 106 | println(err.Error()) 107 | os.Exit(1) 108 | } 109 | defer rl.Close() 110 | 111 | var p Program 112 | fset, err := flagly.Compile("", &p) 113 | if err != nil { 114 | println(err.Error()) 115 | os.Exit(1) 116 | } 117 | rl.Config.AutoComplete = &readline.SegmentComplete{fset.Completer()} 118 | 119 | for { 120 | line, err := rl.Readline() 121 | if err != nil { 122 | break 123 | } 124 | if line == "" { 125 | continue 126 | } 127 | command, err := shlex.Split(line) 128 | if err != nil { 129 | println("error: " + err.Error()) 130 | continue 131 | } 132 | if err := fset.Run(command); err != nil { 133 | println(err.Error()) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /flagly.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "reflect" 9 | "sync" 10 | ) 11 | 12 | const ( 13 | handlerPkgPath = "flagly.Handler" 14 | flaglyHandler = "flaglyHandler" 15 | ) 16 | 17 | var ( 18 | ErrMustAFunc = errors.New("argument must be a func") 19 | ErrFuncOutMustAError = errors.New("FlaglyHandle(Context).out must be a error or nothing: %v") 20 | 21 | ErrMustAPtrToStruct = errors.New("must a pointer to struct") 22 | ErrMustAStruct = errors.New("must a struct") 23 | ) 24 | 25 | func Exit(info interface{}) { 26 | println(fmt.Sprint(info)) 27 | os.Exit(2) 28 | } 29 | 30 | func Bind(target interface{}) { 31 | if err := BindByArgs(target, os.Args); err != nil { 32 | Exit(err) 33 | } 34 | } 35 | 36 | func Run(target interface{}, context ...interface{}) { 37 | if err := RunByArgs(target, os.Args, context...); err != nil { 38 | Exit(err) 39 | } 40 | } 41 | 42 | func BindByArgs(target interface{}, args []string) error { 43 | if args == nil { 44 | args = []string{""} 45 | } 46 | fset, err := Compile(args[0], target) 47 | if err != nil { 48 | return err 49 | } 50 | ptr := reflect.ValueOf(target) 51 | if err := fset.Bind(ptr, args[1:]); err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func RunByArgs(target interface{}, args []string, context ...interface{}) error { 58 | fset, err := Compile(args[0], target) 59 | if err != nil { 60 | return err 61 | } 62 | fset.Context(context...) 63 | if err := fset.Run(args[1:]); err != nil { 64 | return err 65 | } 66 | return nil 67 | } 68 | 69 | func Compile(name string, target interface{}) (*FlaglySet, error) { 70 | fset := New(name) 71 | if err := fset.Compile(target); err != nil { 72 | return nil, err 73 | } 74 | return fset, nil 75 | } 76 | 77 | type FlaglySet struct { 78 | subHandler *Handler 79 | } 80 | 81 | func New(name string) *FlaglySet { 82 | fset := &FlaglySet{ 83 | subHandler: NewHandler(name), 84 | } 85 | return fset 86 | } 87 | 88 | func (f *FlaglySet) Completer() *HandlerCompleter { 89 | return &HandlerCompleter{f.subHandler} 90 | } 91 | 92 | func (f *FlaglySet) Handler() *Handler { 93 | return f.subHandler 94 | } 95 | 96 | func (f *FlaglySet) Context(obj ...interface{}) { 97 | for _, o := range obj { 98 | f.subHandler.Context(o) 99 | } 100 | } 101 | 102 | func (f *FlaglySet) Lambda(name string, fn func() []string) { 103 | f.subHandler.Lambda(name, fn) 104 | } 105 | 106 | func (f *FlaglySet) Compile(target interface{}) error { 107 | return f.subHandler.Compile(reflect.TypeOf(target)) 108 | } 109 | 110 | func (f *FlaglySet) AddSubHandler(command string, hf interface{}) *Handler { 111 | return f.subHandler.AddSubHandler(command, hf) 112 | } 113 | 114 | func (f *FlaglySet) Add(h *Handler) { 115 | f.subHandler.AddHandler(h) 116 | } 117 | 118 | func (f *FlaglySet) SetHandleFunc(hf interface{}) { 119 | f.subHandler.SetHandleFunc(hf) 120 | } 121 | 122 | func (f *FlaglySet) Bind(value reflect.Value, args []string) error { 123 | return f.subHandler.Bind(value, args) 124 | } 125 | 126 | func (f *FlaglySet) Run(args []string) (err error) { 127 | return f.RunWithContext(args, nil) 128 | } 129 | 130 | func (f *FlaglySet) RunWithContext(args []string, context map[string]reflect.Value) (err error) { 131 | stack := []reflect.Value{} 132 | if err = f.subHandler.Run(&stack, args, context); err != nil { 133 | return err 134 | } 135 | return 136 | } 137 | 138 | func (f *FlaglySet) GetHandler(name string) *Handler { 139 | return f.subHandler.GetHandler(name) 140 | } 141 | 142 | func (f *FlaglySet) Usage() string { 143 | buffer := bytes.NewBuffer(nil) 144 | f.subHandler.usage(buffer, "") 145 | return buffer.String() 146 | } 147 | 148 | func (f *FlaglySet) Close() { 149 | f.subHandler.Close() 150 | } 151 | 152 | func IsShowUsage(err error) *showUsageError { 153 | if s, ok := err.(*showUsageError); ok { 154 | return s 155 | } 156 | if s, ok := err.(showUsageError); ok { 157 | return &s 158 | } 159 | return nil 160 | } 161 | 162 | // ----------------------------------------------------------------------------- 163 | 164 | var ( 165 | descMap = map[uintptr]string{} 166 | descGuard sync.Mutex 167 | ) 168 | 169 | func SetDesc(target interface{}, desc string) { 170 | value := reflect.ValueOf(target) 171 | if value.Kind() != reflect.Ptr { 172 | panic("SetDesc only accept pointer") 173 | } 174 | value = reflect.ValueOf(target).Elem() 175 | descGuard.Lock() 176 | descMap[value.UnsafeAddr()] = desc 177 | descGuard.Unlock() 178 | } 179 | 180 | func getDescMap() map[uintptr]string { 181 | descGuard.Lock() 182 | ret := descMap 183 | descMap = map[uintptr]string{} 184 | descGuard.Unlock() 185 | return ret 186 | } 187 | 188 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chzyer/flagly 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.6 6 | 7 | require ( 8 | github.com/chzyer/logex v1.2.1 9 | github.com/chzyer/readline v1.5.1 10 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 11 | ) 12 | 13 | require ( 14 | github.com/chzyer/test v1.0.0 // indirect 15 | golang.org/x/sys v0.31.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 2 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 3 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 4 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 5 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 6 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 7 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 8 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 9 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 10 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 11 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 12 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | EmptyError error 13 | EmptyType reflect.Type 14 | EmptyValue reflect.Value 15 | IfaceError = reflect.TypeOf(&EmptyError).Elem() 16 | HandlerType = reflect.TypeOf(new(Handler)) 17 | ) 18 | 19 | type Handler struct { 20 | Parent *Handler 21 | Name string 22 | Desc string 23 | Children []*Handler 24 | 25 | context map[string]reflect.Value 26 | lambdaMap map[string]func() []string 27 | Options []*Option 28 | OptionType reflect.Type 29 | handleFunc reflect.Value 30 | onGetChildren func(*Handler) []*Handler 31 | onExit func() 32 | } 33 | 34 | func NewHandler(name string) *Handler { 35 | h := &Handler{ 36 | Name: name, 37 | context: make(map[string]reflect.Value), 38 | lambdaMap: make(map[string]func() []string), 39 | } 40 | return h 41 | } 42 | 43 | func (h *Handler) GetRoot() *Handler { 44 | if h.Parent != nil { 45 | return h.Parent.GetRoot() 46 | } 47 | return h 48 | } 49 | 50 | func (h *Handler) GetName() string { 51 | return h.Name 52 | } 53 | 54 | func (h *Handler) findArgOption() *Option { 55 | for _, op := range h.Options { 56 | if op.IsArg() { 57 | return op 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | func (h *Handler) GetTreeChildren() []Tree { 64 | children := h.GetChildren() 65 | if len(children) == 0 { 66 | argOp := h.findArgOption() 67 | if argOp != nil { 68 | return argOp.GetTree(h.lambdaMap) 69 | } 70 | } 71 | ret := make([]Tree, len(children)) 72 | for idx, ch := range children { 73 | ret[idx] = ch 74 | } 75 | return ret 76 | } 77 | 78 | func (h *Handler) SetOnExit(f func()) { 79 | h.onExit = f 80 | } 81 | 82 | func (h *Handler) ResetHandler() { 83 | h.Children = nil 84 | } 85 | 86 | // combine NewHander(name).SetHanderFunc()/AddHandler(subHandler) 87 | func (h *Handler) AddSubHandler(name string, function interface{}) *Handler { 88 | subHandler := NewHandler(name) 89 | subHandler.SetHandleFunc(function) 90 | h.AddHandler(subHandler) 91 | return subHandler 92 | } 93 | 94 | func (h *Handler) copyContext() { 95 | for _, ch := range h.Children { 96 | ch.context = h.context 97 | ch.lambdaMap = h.lambdaMap 98 | ch.copyContext() 99 | } 100 | } 101 | 102 | func (h *Handler) Lambda(name string, fn func() []string) { 103 | h.lambdaMap[name] = fn 104 | } 105 | 106 | func (h *Handler) AddHandler(child *Handler) { 107 | child.Parent = h 108 | h.Children = append(h.Children, child) 109 | h.copyContext() 110 | child.EnsureHelpOption() 111 | } 112 | 113 | func (h *Handler) SetGetChildren(f func(*Handler) []*Handler) { 114 | h.onGetChildren = f 115 | } 116 | 117 | func (h *Handler) SetOptionType(option reflect.Type) error { 118 | op := option 119 | if op.Kind() == reflect.Ptr { 120 | op = op.Elem() 121 | } 122 | if op.Kind() == reflect.Struct { 123 | if op.String() != handlerPkgPath { 124 | h.OptionType = option 125 | } 126 | } 127 | if h.OptionType != nil { 128 | var err error 129 | h.Options, err = h.parseOption() 130 | if err != nil { 131 | return err 132 | } 133 | 134 | if IsImplementDescer(h.OptionType) { 135 | op := h.OptionType 136 | if op.Kind() == reflect.Ptr { 137 | op = op.Elem() 138 | } 139 | value := reflect.New(op) 140 | h.Desc = value.Interface().(FlaglyDescer).FlaglyDesc() 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | // only func is accepted 147 | // 1. func() error 148 | // 2. func(*struct) error 149 | // 3. func(*struct, *flagly.Handler) error 150 | // 4. func(*flagly.Handler) error 151 | // 5. func(Handler, Context) error 152 | func (h *Handler) SetHandleFunc(obj interface{}) { 153 | err := h.setHandleFunc(reflect.ValueOf(obj)) 154 | if err != nil { 155 | panic(err) 156 | } 157 | } 158 | 159 | func (h *Handler) setHandleFunc(funcValue reflect.Value) error { 160 | if funcValue.Kind() != reflect.Func { 161 | return ErrMustAFunc 162 | } 163 | t := funcValue.Type() 164 | if t.NumOut() != 1 || !t.Out(0).Implements(IfaceError) { 165 | if t.NumOut() != 0 { 166 | return fmt.Errorf(ErrFuncOutMustAError.Error(), h.Name) 167 | } 168 | } 169 | if t.NumIn() >= 1 { 170 | if err := h.SetOptionType(t.In(0)); err != nil { 171 | return err 172 | } 173 | } 174 | h.handleFunc = funcValue 175 | return nil 176 | } 177 | 178 | func (h *Handler) findEnterFunc(t reflect.Type) *reflect.Method { 179 | method, ok := t.MethodByName(flaglyEnter) 180 | if ok { 181 | return &method 182 | } 183 | return nil 184 | } 185 | 186 | func (h *Handler) findHandleFunc(t reflect.Type) *reflect.Method { 187 | for i := 0; i < t.NumMethod(); i++ { 188 | method := t.Method(i) 189 | if method.Name == flaglyHandle { 190 | return &method 191 | } 192 | } 193 | return nil 194 | } 195 | 196 | func (h *Handler) EnsureHelpOption() { 197 | h.Options = h.tryToAddHelpOption(h.Options) 198 | } 199 | 200 | func (h *Handler) tryToAddHelpOption(ops []*Option) []*Option { 201 | if len(h.GetChildren()) == 0 { 202 | hasHelp := false 203 | for _, op := range ops { 204 | if op.Name == "h" { 205 | hasHelp = true 206 | break 207 | } 208 | } 209 | if !hasHelp { 210 | ops = append(ops, NewHelpFlag()) 211 | } 212 | } 213 | return ops 214 | } 215 | 216 | func (h *Handler) CompileIface(obj interface{}) error { 217 | return h.Compile(reflect.TypeOf(obj)) 218 | } 219 | 220 | func (h *Handler) log(obj interface{}) { 221 | println(fmt.Sprintf("[handler:%v] %v", h.Name, obj)) 222 | } 223 | 224 | func SetContext(vals map[string]reflect.Value, obj interface{}) { 225 | value := reflect.ValueOf(obj) 226 | typ := value.Type() 227 | if typ.Kind() == reflect.Ptr { 228 | vals[typ.String()] = value 229 | vals[typ.Elem().String()] = value 230 | } else { 231 | vals[typ.String()] = value 232 | vals[reflect.PtrTo(typ).String()] = value 233 | } 234 | } 235 | 236 | func (h *Handler) Context(obj interface{}) { 237 | SetContext(h.context, obj) 238 | } 239 | 240 | func (h *Handler) Compile(t reflect.Type) error { 241 | method := h.findHandleFunc(t) 242 | if method != nil { 243 | if err := h.setHandleFunc(method.Func); err != nil { 244 | return err 245 | } 246 | } else { 247 | if err := h.SetOptionType(t); err != nil { 248 | return err 249 | } 250 | } 251 | 252 | if t.Kind() == reflect.Ptr { 253 | t = t.Elem() 254 | } 255 | for i := 0; i < t.NumField(); i++ { 256 | field := t.Field(i) 257 | tag := StructTag(field.Tag) 258 | if tag.FlaglyHas("handler") { 259 | name := tag.GetName() 260 | if name == "" { 261 | name = strings.ToLower(field.Name) 262 | } 263 | subh := NewHandler(name) 264 | if err := subh.Compile(field.Type); err != nil { 265 | return err 266 | } 267 | h.AddHandler(subh) 268 | } 269 | } 270 | 271 | h.Options = h.tryToAddHelpOption(h.Options) 272 | 273 | return nil 274 | } 275 | 276 | func (h *Handler) HasDesc() bool { 277 | return h.Desc != "" 278 | } 279 | 280 | func (h *Handler) writeDesc(buf *bytes.Buffer) { 281 | prefix := " " 282 | space := 20 283 | if len(h.Name) > space { 284 | buf.WriteString(prefix + h.Name + "\n") 285 | if h.HasDesc() { 286 | buf.WriteString(strings.Repeat(" ", space) + h.Desc + "\n") 287 | } 288 | } else { 289 | indent := strings.Repeat(" ", space-len(h.Name)) 290 | buf.WriteString(prefix + h.Name + indent + h.Desc + "\n") 291 | } 292 | } 293 | 294 | func (h *Handler) GetChildren() []*Handler { 295 | if h.onGetChildren != nil { 296 | ch := h.onGetChildren(h) 297 | for _, c := range ch { 298 | h.AddHandler(c) 299 | } 300 | return ch 301 | } 302 | return h.Children 303 | } 304 | 305 | func (h *Handler) GetHandler(name string) *Handler { 306 | for _, ch := range h.GetChildren() { 307 | if ch.Name == name { 308 | return ch 309 | } 310 | } 311 | return nil 312 | } 313 | 314 | func (h *Handler) Usage(prefix string) string { 315 | buf := bytes.NewBuffer(nil) 316 | err := h.usage(buf, prefix) 317 | if err != nil { 318 | println(err.Error()) 319 | os.Exit(2) 320 | } 321 | return buf.String() 322 | } 323 | 324 | func (h *Handler) parseOption() ([]*Option, error) { 325 | if h.OptionType == EmptyType { 326 | return nil, nil 327 | } 328 | ops, err := ParseStructToOptions(h, h.OptionType) 329 | return ops, err 330 | } 331 | 332 | func (h *Handler) GetOptionNames() []string { 333 | ret := make([]string, 0, len(h.Options)) 334 | for _, op := range h.Options { 335 | ret = append(ret, op.Name) 336 | } 337 | return ret 338 | } 339 | 340 | func (h *Handler) findOption(name string) int { 341 | for idx, op := range h.Options { 342 | if op.Name == name { 343 | return idx 344 | } 345 | } 346 | return -1 347 | } 348 | 349 | func (h *Handler) parseToStruct(v reflect.Value, args []string) ([]string, error) { 350 | tokens := make([][]string, len(h.Options)) 351 | idx := 0 352 | for ; idx < len(args); idx++ { 353 | arg := args[idx] 354 | if strings.HasPrefix(arg, "-") { 355 | // TODO: flag name can't be - 356 | if arg == "--" { 357 | args = args[idx+1:] 358 | break 359 | } 360 | opIdx := h.findOption(arg[1:]) 361 | if opIdx < 0 { 362 | continue 363 | } 364 | op := h.Options[opIdx] 365 | if op.ShowUsage { 366 | return args, ErrShowUsage 367 | } 368 | min, max := op.Typer.NumArgs() 369 | subArgs := make([]string, 0, max) 370 | for i := idx + 1; i <= idx+max && i < len(args); i++ { 371 | if !op.Typer.CanBeValue(args[i]) { 372 | break 373 | } 374 | subArgs = append(subArgs, args[i]) 375 | } 376 | if len(subArgs) < min { 377 | return args, fmt.Errorf("args missing") 378 | } 379 | idx += len(subArgs) 380 | tokens[opIdx] = subArgs 381 | continue 382 | } 383 | break 384 | } 385 | 386 | args = args[idx:] 387 | 388 | for idx, op := range h.Options { 389 | var err error 390 | if op.IsArg() { 391 | if op.ArgIdx == -1 { 392 | err = op.BindTo(v, args) 393 | } else if op.ArgIdx >= len(args) { 394 | if op.HasDefault() { 395 | err = op.BindTo(v, nil) 396 | } else { 397 | // do nothing 398 | } 399 | } else { 400 | err = op.BindTo(v, args[op.ArgIdx:op.ArgIdx+1]) 401 | } 402 | } else if op.IsFlag() { 403 | err = op.BindTo(v, tokens[idx]) 404 | } else { 405 | return args, fmt.Errorf("invalid option type: %v", op.Type) 406 | } 407 | if err != nil { 408 | return nil, err 409 | } 410 | } 411 | if IsImplementVerifier(v.Type()) { 412 | if err := v.Interface().(FlaglyVerifier).FlaglyVerify(); err != nil { 413 | return args, Error(err.Error()) 414 | } 415 | } 416 | return args, nil 417 | } 418 | 419 | func (h *Handler) bindStackToStruct(stack []reflect.Value, value reflect.Value) { 420 | if value.Kind() == reflect.Ptr { 421 | value = value.Elem() 422 | } 423 | t := value.Type() 424 | for i := 0; i < value.NumField(); i++ { 425 | field := t.Field(i) 426 | tag := StructTag(field.Tag) 427 | if tag.GetName() == flaglyParentName || 428 | tag.FlaglyHas("parent") { 429 | for _, s := range stack { 430 | if s.Type().String() == field.Type.String() { 431 | value.Field(i).Set(s) 432 | } 433 | } 434 | } 435 | } 436 | } 437 | 438 | func (h *Handler) RawCall(vals []reflect.Value) error { 439 | if !h.handleFunc.IsValid() { 440 | return ErrShowUsage 441 | } 442 | out := h.handleFunc.Call(vals) 443 | if len(out) != 0 { 444 | if err, ok := out[0].Interface().(error); ok { 445 | return err 446 | } 447 | } 448 | return nil 449 | } 450 | 451 | func (h *Handler) Call(stack []reflect.Value, args []string, context map[string]reflect.Value) error { 452 | if h.handleFunc.IsValid() { 453 | t := h.handleFunc.Type() 454 | numIn := t.NumIn() 455 | ins := make([]reflect.Value, numIn) 456 | for i := 0; i < numIn; i++ { 457 | tIn := t.In(i) 458 | if i == 0 { 459 | ins[0] = stack[len(stack)-1] 460 | if tIn.Kind() != reflect.Ptr { 461 | ins[0] = ins[0].Elem() 462 | } 463 | continue 464 | } 465 | switch tIn.String() { 466 | case "*" + handlerPkgPath: 467 | // TODO: must be a pointer 468 | ins[i] = reflect.ValueOf(h) 469 | default: 470 | val, ok := context[tIn.String()] 471 | if !ok { 472 | val, ok = h.context[tIn.String()] 473 | } 474 | if !ok { 475 | ins[i] = reflect.Zero(t.In(i)) 476 | } else { 477 | if val.Type().String() != tIn.String() { 478 | if strings.HasPrefix(tIn.String(), "*") { 479 | // want a pointer 480 | // todo, make a pointer 481 | val = reflect.New(tIn) 482 | } else { 483 | val = val.Elem() 484 | } 485 | } 486 | ins[i] = val 487 | } 488 | } 489 | } 490 | // first argument is a struct 491 | out := h.handleFunc.Call(ins) 492 | if len(out) != 0 { 493 | if err, ok := out[0].Interface().(error); ok { 494 | return err 495 | } 496 | } 497 | return nil 498 | } else { 499 | // show usage 500 | return ErrShowUsage 501 | } 502 | } 503 | 504 | func (h *Handler) GetCommands() []string { 505 | return h.getChildNames() 506 | } 507 | 508 | func (h *Handler) getChildNames() (names []string) { 509 | for _, n := range h.GetChildren() { 510 | names = append(names, n.Name) 511 | } 512 | return names 513 | } 514 | 515 | func (h *Handler) Run(stack *[]reflect.Value, args []string, context map[string]reflect.Value) (err error) { 516 | defer func() { 517 | if e := IsShowUsage(err); e != nil { 518 | err = e.Trace(h) 519 | } 520 | }() 521 | runed := false 522 | var value reflect.Value 523 | if h.OptionType != nil { 524 | t := h.OptionType 525 | if t.Kind() == reflect.Ptr { 526 | t = t.Elem() 527 | } 528 | value = reflect.New(t) 529 | args, err = h.parseToStruct(value, args) 530 | if err != nil { 531 | return err 532 | } 533 | } 534 | *stack = append(*stack, value) 535 | 536 | if len(args) > 0 { 537 | enter := h.findEnterFunc(h.OptionType) 538 | if enter != nil { 539 | args := make([]reflect.Value, 1) 540 | args[0] = value 541 | enter.Func.Call(args) 542 | } 543 | for _, ch := range h.GetChildren() { 544 | if args[0] == ch.Name { 545 | err = ch.Run(stack, args[1:], context) 546 | runed = true 547 | break 548 | } 549 | } 550 | } 551 | if !runed { 552 | err = h.Call(*stack, args, context) 553 | } 554 | return err 555 | } 556 | 557 | func (h *Handler) String() string { 558 | return fmt.Sprintf("%+v", *h) 559 | } 560 | 561 | func (h *Handler) HasFlagOptions() bool { 562 | if len(h.Options) > 0 { 563 | for _, op := range h.Options { 564 | if op.IsFlag() { 565 | return true 566 | } 567 | } 568 | } 569 | return false 570 | } 571 | 572 | func (h *Handler) UsagePrefix() string { 573 | buf := bytes.NewBuffer(nil) 574 | buf.WriteString(h.Name) 575 | if h.HasFlagOptions() { 576 | buf.WriteString(" [" + h.Name + " option]") 577 | } 578 | return buf.String() 579 | } 580 | 581 | func (h *Handler) HasArgOptions() bool { 582 | if len(h.Options) > 0 { 583 | for _, op := range h.Options { 584 | if op.IsArg() { 585 | return true 586 | } 587 | } 588 | } 589 | return false 590 | } 591 | 592 | func (h *Handler) usage(buf *bytes.Buffer, prefix string) error { 593 | if prefix != "" { 594 | prefix += " " 595 | } 596 | 597 | hasFlags := h.HasFlagOptions() 598 | hasArgs := h.HasArgOptions() 599 | children := h.GetChildren() 600 | hasCommands := len(children) > 0 601 | 602 | if h.Name != "" { 603 | buf.WriteString("usage: " + prefix + h.Name) 604 | if hasFlags { 605 | buf.WriteString(" [option]") 606 | } 607 | if hasCommands { 608 | buf.WriteString(" ") 609 | } 610 | if hasArgs { 611 | if hasFlags { 612 | buf.WriteString(" [--]") 613 | } 614 | for _, op := range h.Options { 615 | if op.IsArg() { 616 | buf.WriteString(" ") 617 | if op.HasDefault() { 618 | buf.WriteString("[") 619 | } 620 | buf.WriteString("<" + op.Name + ">") 621 | if op.HasDefault() { 622 | buf.WriteString("]") 623 | } 624 | } 625 | } 626 | } 627 | buf.WriteString("\n") 628 | } 629 | 630 | if h.Desc != "" { 631 | buf.WriteString(h.Desc) 632 | buf.WriteString("\n") 633 | } 634 | 635 | if hasFlags { 636 | buf.WriteString(h.usageOptions("options")) 637 | } 638 | 639 | if hasCommands { 640 | buf.WriteString("\ncommands:\n") 641 | for _, ch := range children { 642 | ch.writeDesc(buf) 643 | } 644 | } 645 | 646 | return nil 647 | 648 | } 649 | 650 | func (h *Handler) usageOptions(name string) string { 651 | buf := bytes.NewBuffer(nil) 652 | buf.WriteString("\n" + name + ":\n") 653 | for _, op := range h.Options { 654 | op.usage(buf) 655 | buf.WriteString("\n") 656 | } 657 | return buf.String() 658 | } 659 | 660 | func (h *Handler) Close() { 661 | if h.onExit != nil { 662 | h.onExit() 663 | } 664 | for _, ch := range h.GetChildren() { 665 | ch.Close() 666 | } 667 | } 668 | 669 | func (h *Handler) Bind(ptr reflect.Value, args []string) (err error) { 670 | if ptr.Kind() != reflect.Ptr { 671 | return ErrMustAPtrToStruct 672 | } 673 | defer func() { 674 | if e := IsShowUsage(err); e != nil { 675 | err = e.Trace(h) 676 | } 677 | }() 678 | if h.OptionType != nil { 679 | t := h.OptionType 680 | if t.Kind() == reflect.Ptr { 681 | t = t.Elem() 682 | } 683 | value := reflect.New(t) 684 | _, err = h.parseToStruct(value, args) 685 | if err != nil { 686 | return err 687 | } 688 | 689 | ptr.Elem().Set(value.Elem()) 690 | } 691 | return nil 692 | } 693 | 694 | -------------------------------------------------------------------------------- /iface.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import "reflect" 4 | 5 | var ( 6 | emptyFlaglyIniter = reflect.TypeOf(new(FlaglyIniter)).Elem() 7 | emptyFlaglyDescer = reflect.TypeOf(new(FlaglyDescer)).Elem() 8 | emptyFlaglyVerifier = reflect.TypeOf(new(FlaglyVerifier)).Elem() 9 | FlaglyIniterName = "FlaglyInit" 10 | flaglyHandle = "FlaglyHandle" 11 | flaglyEnter = "FlaglyEnter" 12 | ) 13 | 14 | type FlaglyIniter interface { 15 | FlaglyInit() 16 | } 17 | 18 | type FlaglyDescer interface { 19 | FlaglyDesc() string 20 | } 21 | 22 | type FlaglyVerifier interface { 23 | FlaglyVerify() error 24 | } 25 | 26 | func IsImplementIniter(t reflect.Type) bool { 27 | return IsImplemented(t, emptyFlaglyIniter) 28 | } 29 | 30 | func IsImplementDescer(t reflect.Type) bool { 31 | return IsImplemented(t, emptyFlaglyDescer) 32 | } 33 | 34 | func IsImplementVerifier(t reflect.Type) bool { 35 | return IsImplemented(t, emptyFlaglyVerifier) 36 | } 37 | 38 | func IsImplemented(t, target reflect.Type) bool { 39 | if t.Implements(target) { 40 | return true 41 | } 42 | if t.Kind() == reflect.Struct { 43 | return reflect.PtrTo(t).Implements(target) 44 | } else if t.Kind() == reflect.Ptr { 45 | return t.Elem().Implements(target) 46 | } 47 | return false 48 | } 49 | -------------------------------------------------------------------------------- /option.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | flaglyPrefix = "flagly" 13 | flaglyParentName = "flaglyParent" 14 | ) 15 | 16 | type OptionType int 17 | 18 | func (t OptionType) String() string { 19 | switch t { 20 | case FlagOption: 21 | return "flag" 22 | case ArgOption: 23 | return "arg" 24 | default: 25 | return "" 26 | } 27 | } 28 | 29 | const ( 30 | FlagOption OptionType = iota 31 | ArgOption 32 | ) 33 | 34 | type Option struct { 35 | Index int 36 | Name string 37 | LongName string 38 | Type OptionType 39 | BindType reflect.Type 40 | Typer Typer 41 | Desc string 42 | Default *string 43 | ArgName *string 44 | ArgIdx int 45 | ShowUsage bool 46 | Tag StructTag 47 | } 48 | 49 | func NewHelpFlag() *Option { 50 | op, err := NewFlag("h", reflect.TypeOf(true)) 51 | if err != nil { 52 | panic(err) 53 | } 54 | op.ShowUsage = true 55 | op.Desc = "show help" 56 | return op 57 | } 58 | 59 | func NewFlag(name string, bind reflect.Type) (*Option, error) { 60 | op := &Option{ 61 | Index: -1, 62 | Name: name, 63 | BindType: bind, 64 | Type: FlagOption, 65 | } 66 | if err := op.init(); err != nil { 67 | return nil, err 68 | } 69 | return op, nil 70 | } 71 | 72 | func NewArg(name string, idx int, bind reflect.Type) (*Option, error) { 73 | op := &Option{ 74 | Index: -1, 75 | Name: name, 76 | Type: ArgOption, 77 | BindType: bind, 78 | ArgIdx: idx, 79 | } 80 | if err := op.init(); err != nil { 81 | return nil, err 82 | } 83 | return op, nil 84 | } 85 | 86 | func (o *Option) init() error { 87 | typer, err := GetTyper(o.BindType) 88 | if err != nil { 89 | return err 90 | } 91 | o.Typer = typer 92 | return nil 93 | } 94 | 95 | func (o *Option) GetTree(lambdaMap map[string]func() []string) []Tree { 96 | var candidates []string 97 | if selectCall := o.Tag.Get("selectCall"); selectCall != "" { 98 | if fn := lambdaMap[selectCall]; fn != nil { 99 | candidates = fn() 100 | } 101 | } 102 | if tags := o.Tag.Get("select"); tags != "" { 103 | candidates = strings.Split(tags, ",") 104 | } 105 | trees := make([]Tree, len(candidates)) 106 | for idx, tag := range candidates { 107 | trees[idx] = StringTree(tag) 108 | } 109 | return trees 110 | } 111 | 112 | func (o *Option) BindTo(value reflect.Value, args []string) error { 113 | if o.Index < 0 { 114 | return nil 115 | } 116 | f := value.Elem().Field(o.Index) 117 | if args == nil { 118 | if o.HasDefault() { 119 | return o.Typer.Set(f, []string{*o.Default}) 120 | } 121 | } else { 122 | return o.Typer.Set(f, args) 123 | } 124 | return nil 125 | } 126 | 127 | func (o *Option) HasArgName() bool { 128 | return o.ArgName != nil 129 | } 130 | 131 | func (o *Option) HasDefault() bool { 132 | return o.Default != nil 133 | } 134 | 135 | func (o *Option) IsFlag() bool { 136 | return o.Type == FlagOption 137 | } 138 | 139 | func (o *Option) IsArg() bool { 140 | return o.Type == ArgOption 141 | } 142 | 143 | func (o *Option) HasDesc() bool { 144 | return o.Desc != "" 145 | } 146 | 147 | func (o *Option) usage(buf *bytes.Buffer) { 148 | b := bytes.NewBuffer(nil) 149 | b.WriteString(" ") 150 | length := 4 + 20 151 | if o.IsFlag() { 152 | b.WriteString("-" + o.Name) 153 | min, _ := o.Typer.NumArgs() 154 | 155 | if min > 0 { 156 | if o.HasArgName() { 157 | if o.HasDefault() { 158 | b.WriteString(fmt.Sprintf(" <%v=%v>", *o.ArgName, *o.Default)) 159 | } else { 160 | b.WriteString(fmt.Sprintf(" <%v>", *o.ArgName)) 161 | } 162 | } else if o.HasDefault() { 163 | b.WriteString(fmt.Sprintf(` "%v"`, *o.Default)) 164 | } else { 165 | 166 | } 167 | } 168 | } else if o.IsArg() { 169 | b.WriteString(o.Name) 170 | if o.Tag.Get("select") != "" { 171 | o.Desc = o.Tag.Get("select") 172 | } 173 | } 174 | 175 | if o.HasDesc() { 176 | if b.Len() > length { 177 | b.WriteString("\n" + strings.Repeat(" ", length)) 178 | } else { 179 | b.WriteString(strings.Repeat(" ", length-b.Len())) 180 | } 181 | b.WriteString(o.Desc) 182 | } 183 | 184 | b.WriteTo(buf) 185 | } 186 | 187 | func IsWrapBy(s, ch2 string) bool { 188 | return len(s) >= 2 && s[0] == ch2[0] && s[len(s)-1] == ch2[1] 189 | } 190 | 191 | func GetIdxInArray(s string) int { 192 | s = s[1 : len(s)-1] 193 | idx, err := strconv.Atoi(s) 194 | if err != nil { 195 | return -1 196 | } 197 | return idx 198 | } 199 | 200 | func GetMethod(s reflect.Value, name string) reflect.Value { 201 | method := s.MethodByName(name) 202 | if !method.IsValid() { 203 | method = s.Elem().MethodByName(name) 204 | } 205 | return method 206 | } 207 | 208 | func ParseStructToOptions(h *Handler, t reflect.Type) (ret []*Option, err error) { 209 | if t.Kind() == reflect.Ptr { 210 | t = t.Elem() 211 | } 212 | 213 | descIdx := make(map[int]string) 214 | 215 | value := reflect.New(t) 216 | method := GetMethod(value, FlaglyIniterName) 217 | if method.IsValid() { 218 | methodType := method.Type() 219 | args := make([]reflect.Value, methodType.NumIn()) 220 | for i := 0; i < methodType.NumIn(); i++ { 221 | typ := methodType.In(i) 222 | if typ.String() == HandlerType.String() { 223 | args[i] = reflect.ValueOf(h) 224 | } else { 225 | args[i] = reflect.Zero(typ) 226 | } 227 | } 228 | method.Call(args) 229 | descMap := getDescMap() 230 | elem := value.Elem() 231 | for i := 0; i < elem.NumField(); i++ { 232 | desc, ok := descMap[elem.Field(i).UnsafeAddr()] 233 | if !ok { 234 | continue 235 | } 236 | descIdx[i] = desc 237 | } 238 | } 239 | 240 | for i := 0; i < t.NumField(); i++ { 241 | field := t.Field(i) 242 | tag := StructTag(field.Tag) 243 | if strings.HasPrefix(tag.GetName(), flaglyPrefix) || tag.Has("flagly") { 244 | continue 245 | } 246 | 247 | name := tag.GetName() 248 | if name == "" { 249 | name = strings.ToLower(field.Name) 250 | } else if name == "-" { 251 | continue 252 | } 253 | var op *Option 254 | 255 | if IsWrapBy(tag.Get("type"), "[]") { 256 | op, err = NewArg(name, GetIdxInArray(tag.Get("type")), field.Type) 257 | } else { 258 | op, err = NewFlag(name, field.Type) 259 | } 260 | if err != nil { 261 | return nil, err 262 | } 263 | op.Tag = tag 264 | 265 | if op.Name == "-" { 266 | return nil, fmt.Errorf(`name "-" is not allowed`) 267 | } 268 | op.Index = i 269 | 270 | op.Default = tag.GetPtr("default") 271 | if namer, ok := op.Typer.(BaseTypeArgNamer); ok { 272 | argName := namer.ArgName() 273 | if argName != "" { 274 | op.ArgName = &argName 275 | } 276 | } 277 | 278 | if argName := tag.GetPtr("arg"); argName != nil { 279 | op.ArgName = argName 280 | } 281 | op.Desc = tag.Get("desc") 282 | if desc, ok := descIdx[i]; ok { 283 | op.Desc = desc 284 | } 285 | 286 | ret = append(ret, op) 287 | } 288 | return 289 | } 290 | 291 | func (o *Option) String() string { 292 | return fmt.Sprintf("%v", *o) 293 | } 294 | -------------------------------------------------------------------------------- /tagparser.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | type StructTag string 10 | 11 | func (st StructTag) Flagly() []string { 12 | sp := strings.Split(st.Get("flagly"), ",") 13 | return sp 14 | } 15 | 16 | func (st StructTag) FlaglyHas(name string) bool { 17 | for _, s := range st.Flagly() { 18 | if s == name { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | func (st StructTag) GetPtr(name string) *string { 26 | if !st.Has(name) { 27 | return nil 28 | } 29 | ret := st.Get(name) 30 | return &ret 31 | } 32 | 33 | func (st StructTag) Get(name string) string { 34 | var idx int 35 | s := string(st) 36 | target := name + ":" 37 | for { 38 | idx = strings.Index(s, target) 39 | if idx >= 0 { 40 | if strings.Count(s, `"`)&1 == 0 { 41 | break 42 | } else { 43 | s = s[idx+1:] 44 | } 45 | } else { 46 | return "" 47 | } 48 | } 49 | 50 | // found 51 | quoted := false 52 | content := bytes.NewBuffer(nil) 53 | for i := idx + len(target); i < len(s); i++ { 54 | isQuoteChar := s[i] == '"' 55 | if !quoted && s[i] == ' ' { 56 | break 57 | } 58 | content.WriteByte(s[i]) 59 | if isQuoteChar { 60 | if !quoted { 61 | quoted = true 62 | } else { 63 | quoted = false 64 | break 65 | } 66 | } 67 | } 68 | 69 | ret, err := strconv.Unquote(content.String()) 70 | if err != nil { 71 | ret = content.String() 72 | } 73 | return ret 74 | } 75 | 76 | func (st StructTag) Has(name string) bool { 77 | s := string(st) 78 | var idx int 79 | for { 80 | idx = strings.Index(s, name) 81 | if idx < 0 { 82 | return false 83 | } 84 | if idx > 0 && s[idx-1] != ' ' { 85 | continue 86 | } 87 | if len(s) > idx+len(name) { 88 | ss := s[idx+len(name)] 89 | if ss == ' ' || ss == ':' { 90 | return true 91 | } 92 | } else { 93 | return true 94 | } 95 | s = s[idx+1:] 96 | } 97 | } 98 | 99 | func (st StructTag) GetName() string { 100 | if name := st.getName(); name != "" { 101 | return name 102 | } 103 | return st.Get("name") 104 | } 105 | 106 | func (st StructTag) getName() string { 107 | s := string(st) 108 | idx := strings.Index(s, " ") 109 | if idx >= 0 { 110 | s = s[:idx] 111 | } 112 | if strings.Contains(s, ":") { 113 | return "" 114 | } 115 | if len(s) >= 2 && 116 | s[0] == '"' && s[len(s)-1] == '"' { 117 | tmpName, err := strconv.Unquote(s) 118 | if err == nil { 119 | s = tmpName 120 | } 121 | } 122 | s = strings.TrimSpace(s) 123 | return s 124 | } 125 | -------------------------------------------------------------------------------- /tagparser_test.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import "testing" 4 | 5 | func TestTagParser(t *testing.T) { 6 | s := `bson:"dfdf df df " json:"hello dfdf" xml:dfdf sd` 7 | st := StructTag(s) 8 | if st.Get("json") != "hello dfdf" { 9 | t.Fatal("error") 10 | } 11 | if st.Get("bson") != "dfdf df df " { 12 | t.Fatal("error") 13 | } 14 | if !st.Has("xml") { 15 | t.Fatal("error") 16 | } 17 | if st.Get("xml") != "dfdf" { 18 | t.Fatal("error") 19 | } 20 | 21 | st = StructTag(`quote optional default:"" required`) 22 | if !st.Has("optional") { 23 | t.Fatal("error") 24 | } 25 | if st.GetPtr("default") == nil { 26 | t.Fatal("error") 27 | } 28 | if st.GetPtr("kjkj") != nil { 29 | t.Fatal("error") 30 | } 31 | if st.GetName() != "quote" { 32 | t.Fatal("error") 33 | } 34 | if !st.Has("required") { 35 | t.Fatal("error") 36 | } 37 | 38 | st = StructTag(`"quote"`) 39 | if st.GetName() != "quote" { 40 | t.Fatal("error") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /typer.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var ( 13 | types = map[string]Typer{} 14 | NilValue reflect.Value 15 | ) 16 | 17 | func init() { 18 | RegisterAll(Bool{}, String{}, Duration{}, Int{}, Int64{}, IPNet{}) 19 | Register(MapStringString{}) 20 | } 21 | 22 | func getTypeName(t reflect.Type) string { 23 | if t.Kind() == reflect.Ptr { 24 | t = t.Elem() 25 | } 26 | return t.String() 27 | } 28 | 29 | func RegisterAll(objs ...BaseTyperParser) { 30 | for _, obj := range objs { 31 | Register(ValueWrap{obj}, SliceWrap{obj}) 32 | } 33 | } 34 | 35 | func Register(objs ...Typer) { 36 | for _, obj := range objs { 37 | t := obj.Type() 38 | if t.Kind() == reflect.Ptr { 39 | t = t.Elem() 40 | } 41 | types[getTypeName(t)] = obj 42 | } 43 | } 44 | 45 | func GetTyper(t reflect.Type) (Typer, error) { 46 | name := getTypeName(t) 47 | ret := types[name] 48 | if ret != nil { 49 | return ret, nil 50 | } 51 | return nil, fmt.Errorf("unknown type: %v", name) 52 | } 53 | 54 | type BaseTyper interface { 55 | Type() reflect.Type 56 | } 57 | type BaseTypeArgNamer interface { 58 | ArgName() string 59 | } 60 | type BaseTypeCanBeValuer interface { 61 | CanBeValue(arg string) bool 62 | } 63 | type BaseTypeNumArgs interface { 64 | NumArgs() (int, int) 65 | } 66 | type BaseTyperParser interface { 67 | BaseTyper 68 | ParseArgs(args []string) (reflect.Value, error) 69 | } 70 | 71 | type SliceWrap struct { 72 | BaseTyperParser 73 | } 74 | 75 | func (s SliceWrap) Type() reflect.Type { 76 | return reflect.SliceOf(s.BaseTyperParser.Type()) 77 | } 78 | func (s SliceWrap) Set(source reflect.Value, args []string) error { 79 | if len(args) == 0 { 80 | return nil 81 | } 82 | 83 | for _, a := range args { 84 | arg, err := s.BaseTyperParser.ParseArgs([]string{a}) 85 | if err != nil { 86 | return err 87 | } 88 | val := reflect.New(s.BaseTyperParser.Type()) 89 | SetToSource(val, arg) 90 | source.Set(reflect.Append(source, val.Elem())) 91 | } 92 | return nil 93 | } 94 | 95 | func (s SliceWrap) CanBeValue(arg string) bool { 96 | if cbv, ok := s.BaseTyperParser.(BaseTypeCanBeValuer); ok { 97 | return cbv.CanBeValue(arg) 98 | } 99 | return true 100 | } 101 | 102 | func (s SliceWrap) NumArgs() (int, int) { 103 | if an, ok := s.BaseTyperParser.(BaseTypeNumArgs); ok { 104 | return an.NumArgs() 105 | } 106 | return 1, 1 107 | } 108 | 109 | type Typer interface { 110 | BaseTyper 111 | BaseTypeNumArgs 112 | BaseTypeCanBeValuer 113 | Set(source reflect.Value, args []string) error 114 | } 115 | 116 | type ValueWrap struct{ BaseTyperParser } 117 | 118 | func SetToSource(source, val reflect.Value) { 119 | if source.Kind() == reflect.Ptr { 120 | if source.IsNil() { 121 | ptr := reflect.New(source.Type().Elem()) 122 | source.Set(ptr) 123 | } 124 | if val.Kind() != reflect.Ptr { 125 | source = source.Elem() 126 | } 127 | } 128 | source.Set(val) 129 | } 130 | 131 | func (b ValueWrap) CanBeValue(arg string) bool { 132 | if cbv, ok := b.BaseTyperParser.(BaseTypeCanBeValuer); ok { 133 | return cbv.CanBeValue(arg) 134 | } 135 | return true 136 | } 137 | 138 | func (b ValueWrap) NumArgs() (int, int) { 139 | if an, ok := b.BaseTyperParser.(BaseTypeNumArgs); ok { 140 | return an.NumArgs() 141 | } 142 | return 1, 1 143 | } 144 | 145 | func (b ValueWrap) ArgName() string { 146 | if a, ok := b.BaseTyperParser.(BaseTypeArgNamer); ok { 147 | return a.ArgName() 148 | } 149 | return "" 150 | } 151 | 152 | func (b ValueWrap) Set(source reflect.Value, args []string) error { 153 | val, err := b.ParseArgs(args) 154 | if err != nil { 155 | return err 156 | } 157 | SetToSource(source, val) 158 | return nil 159 | } 160 | 161 | type Int struct{} 162 | 163 | func (Int) Type() reflect.Type { return reflect.TypeOf(int(0)) } 164 | func (Int) ArgName() string { return "number" } 165 | func (Int) ParseArgs(args []string) (reflect.Value, error) { 166 | val, err := strconv.Atoi(args[0]) 167 | if err != nil { 168 | return NilValue, err 169 | } 170 | return reflect.ValueOf(val), nil 171 | } 172 | 173 | type Int64 struct{} 174 | 175 | func (Int64) Type() reflect.Type { return reflect.TypeOf(int64(0)) } 176 | func (Int64) ArgName() string { return "number" } 177 | func (Int64) ParseArgs(args []string) (reflect.Value, error) { 178 | val, err := strconv.ParseInt(args[0], 10, 64) 179 | if err != nil { 180 | return NilValue, err 181 | } 182 | return reflect.ValueOf(val), nil 183 | } 184 | 185 | type String struct{} 186 | 187 | func (String) Type() reflect.Type { return reflect.TypeOf("") } 188 | func (String) CanBeValue(string) bool { return true } 189 | func (String) NumArgs() (int, int) { return 1, 1 } 190 | func (String) ParseArgs(args []string) (reflect.Value, error) { 191 | return reflect.ValueOf(args[0]), nil 192 | } 193 | 194 | type Bool struct{} 195 | 196 | func (Bool) Type() reflect.Type { return reflect.TypeOf(true) } 197 | func (Bool) NumArgs() (int, int) { return 0, 1 } 198 | func (Bool) CanBeValue(arg string) bool { 199 | return arg == "true" || arg == "false" 200 | } 201 | func (Bool) ParseArgs(args []string) (reflect.Value, error) { 202 | val := true 203 | if len(args) == 1 && args[0] == "false" { 204 | val = false 205 | } 206 | return reflect.ValueOf(val), nil 207 | } 208 | 209 | type Duration struct{} 210 | 211 | func (Duration) Type() reflect.Type { return reflect.TypeOf(time.Second) } 212 | func (Duration) NumArgs() (int, int) { return 1, 1 } 213 | func (Duration) CanBeValue(string) bool { return true } 214 | func (Duration) ParseArgs(args []string) (reflect.Value, error) { 215 | a, err := time.ParseDuration(args[0]) 216 | if err != nil { 217 | return NilValue, err 218 | } 219 | return reflect.ValueOf(a), nil 220 | } 221 | 222 | type IPNet struct{} 223 | 224 | func (IPNet) Type() reflect.Type { return reflect.TypeOf(&net.IPNet{}) } 225 | func (IPNet) NumArgs() (int, int) { return 1, 1 } 226 | func (IPNet) CanBeValue(string) bool { return true } 227 | func (IPNet) ParseArgs(args []string) (reflect.Value, error) { 228 | if idx := strings.Index(args[0], "/"); idx < 0 { 229 | args[0] += "/32" 230 | } 231 | 232 | _, ipnet, err := net.ParseCIDR(args[0]) 233 | if err != nil { 234 | return NilValue, err 235 | } 236 | return reflect.ValueOf(ipnet), nil 237 | } 238 | 239 | type MapStringString struct{} 240 | 241 | func (MapStringString) Type() reflect.Type { 242 | return reflect.TypeOf(map[string]string{}) 243 | } 244 | func (MapStringString) NumArgs() (int, int) { return 1, 1 } 245 | func (MapStringString) CanBeValue(string) bool { return true } 246 | func (MapStringString) Set(source reflect.Value, args []string) error { 247 | idx := strings.Index(args[0], "=") 248 | if idx < 0 { 249 | return fmt.Errorf("invalid config: %v", args[0]) 250 | } 251 | elem := source 252 | if source.Kind() == reflect.Ptr { 253 | elem = source.Elem() 254 | } 255 | if elem.IsNil() { 256 | elem.Set(reflect.ValueOf(map[string]string{})) 257 | } 258 | key := args[0][:idx] 259 | value := args[0][idx+1:] 260 | elem.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value)) 261 | return nil 262 | } 263 | func (MapStringString) ArgName() string { 264 | return "key=value" 265 | } 266 | -------------------------------------------------------------------------------- /typer_test.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestTyper(t *testing.T) { 9 | var mapStr map[string]string 10 | mapStrType := reflect.TypeOf(mapStr) 11 | typer, err := GetTyper(mapStrType) 12 | if err != nil { 13 | t.Fatal(err) 14 | } 15 | typer.Set(reflect.ValueOf(&mapStr), []string{"a=b"}) 16 | if mapStr["a"] != "b" { 17 | t.Fatal("error") 18 | } 19 | typer.Set(reflect.ValueOf(&mapStr), []string{"b=c"}) 20 | if mapStr["a"] != "b" || mapStr["b"] != "c" { 21 | t.Fatal("error") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /usage.go: -------------------------------------------------------------------------------- 1 | package flagly 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | ErrShowUsage error = showUsageError{} 10 | ) 11 | 12 | type showUsageError struct { 13 | info string 14 | handlers []*Handler 15 | } 16 | 17 | func (s showUsageError) Error() string { 18 | if s.info != "" { 19 | usage := s.Usage() 20 | if usage != "" { 21 | return s.info + "\n\n" + usage 22 | } 23 | return s.info 24 | 25 | } 26 | return s.Usage() 27 | } 28 | 29 | func (s *showUsageError) Trace(h *Handler) *showUsageError { 30 | s.handlers = append(s.handlers, h) 31 | return s 32 | } 33 | 34 | func (s *showUsageError) Usage() string { 35 | return ShowUsage(s.handlers) 36 | } 37 | 38 | func Errorf(format string, obj ...interface{}) error { 39 | return Error(fmt.Sprintf(format, obj...)) 40 | } 41 | 42 | func Error(info string) error { 43 | return &showUsageError{ 44 | info: info, 45 | } 46 | } 47 | 48 | func ShowUsage(hs []*Handler) string { 49 | prefix := "" 50 | for i := len(hs) - 1; i > 0; i-- { 51 | prefix += hs[i].UsagePrefix() + " " 52 | } 53 | prefix = strings.TrimSpace(prefix) 54 | if len(hs) > 0 { 55 | h := hs[0] 56 | usage := h.Usage(prefix) 57 | for i := 1; i < len(hs); i++ { 58 | if hs[i].HasFlagOptions() { 59 | usage += hs[i].usageOptions(hs[i].Name + " options") 60 | } 61 | } 62 | return usage 63 | } else { 64 | return "" 65 | } 66 | } 67 | --------------------------------------------------------------------------------