├── go.mod ├── config.go ├── context.go ├── go.sum ├── unmarshaler.go ├── flag.go ├── command.go ├── README.md ├── example └── say.go ├── LICENSE └── cortana.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shafreeck/cortana 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/google/btree v1.0.0 7 | github.com/muesli/reflow v0.3.0 8 | ) 9 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package cortana 2 | 3 | type longshort struct { 4 | long string 5 | short string 6 | desc string 7 | } 8 | 9 | type config struct { 10 | path string 11 | unmarshaler Unmarshaler 12 | requireExist bool 13 | } 14 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package cortana 2 | 3 | // desc describes a command 4 | type desc struct { 5 | title string 6 | description string 7 | flags string 8 | } 9 | 10 | type context struct { 11 | name string 12 | args []string 13 | desc desc 14 | longest string // the longest path has been searched 15 | } 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 2 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 3 | github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= 4 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 5 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 6 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 7 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 8 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 9 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 10 | -------------------------------------------------------------------------------- /unmarshaler.go: -------------------------------------------------------------------------------- 1 | package cortana 2 | 3 | // Unmarshaler unmarshals data to v 4 | type Unmarshaler interface { 5 | Unmarshal(data []byte, v interface{}) error 6 | } 7 | 8 | // UnmarshalFunc turns a func to Unmarshaler 9 | type UnmarshalFunc func(data []byte, v interface{}) error 10 | 11 | // Unmarshal the data 12 | func (f UnmarshalFunc) Unmarshal(data []byte, v interface{}) error { 13 | return f(data, v) 14 | } 15 | 16 | // EnvUnmarshaler unmarshals the environment variables 17 | type EnvUnmarshaler interface { 18 | Unmarshal(v interface{}) error 19 | } 20 | 21 | // EnvUnmarshalFunc turns a func to an EnvUnmarshaler 22 | type EnvUnmarshalFunc func(v interface{}) error 23 | 24 | // Unmarshal the environment variables 25 | func (f EnvUnmarshalFunc) Unmarshal(v interface{}) error { 26 | return f(v) 27 | } 28 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package cortana 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | ) 7 | 8 | type flag struct { 9 | name string // the field name 10 | long string 11 | short string 12 | required bool 13 | defaultValue string 14 | description string 15 | rv reflect.Value 16 | } 17 | 18 | // nonflag is in fact a flag without prefix "-" 19 | type nonflag flag 20 | 21 | func parseFlag(tag string, name string, rv reflect.Value) *flag { 22 | f := &flag{name: name, rv: rv} 23 | parts := strings.Split(tag, ",") 24 | 25 | const ( 26 | long = iota 27 | short 28 | defaultValue 29 | description 30 | ) 31 | state := long 32 | for i := 0; i < len(parts); i++ { 33 | p := strings.TrimSpace(parts[i]) 34 | switch state { 35 | case long: 36 | f.long = p 37 | state = short 38 | case short: 39 | f.short = p 40 | state = defaultValue 41 | case defaultValue: 42 | if p == "-" { 43 | f.required = true 44 | } else { 45 | // set to empty value 46 | if p == `''` || p == `""` { 47 | p = "" 48 | } 49 | f.defaultValue = p 50 | } 51 | state = description 52 | case description: 53 | f.description = strings.TrimSpace(strings.Join(parts[i:], ",")) 54 | return f 55 | } 56 | } 57 | return f 58 | } 59 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package cortana 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/google/btree" 7 | ) 8 | 9 | // Command is an executive unit 10 | type Command struct { 11 | Path string 12 | Proc func() 13 | Brief string 14 | Alias bool 15 | order int // the order is the sequence of invoking add command 16 | } 17 | 18 | type command Command 19 | 20 | func (c *command) Less(than btree.Item) bool { 21 | t := than.(*command) 22 | return strings.Compare(c.Path, t.Path) < 0 23 | } 24 | 25 | type commands struct { 26 | t *btree.BTree 27 | } 28 | 29 | func (c commands) scan(prefix string) []*command { 30 | var cmds []*command 31 | begin := &command{Path: prefix} 32 | end := &command{Path: prefix + "\xFF"} 33 | 34 | c.t.AscendRange(begin, end, func(i btree.Item) bool { 35 | cmds = append(cmds, i.(*command)) 36 | return true 37 | }) 38 | return cmds 39 | } 40 | func (c commands) get(path string) *command { 41 | i := c.t.Get(&command{Path: path}) 42 | if i != nil { 43 | return i.(*command) 44 | } 45 | return nil 46 | } 47 | 48 | // orderedCommands keep the order of adding a command 49 | type orderedCommands []*command 50 | 51 | func (cmds orderedCommands) Len() int { 52 | return len(cmds) 53 | } 54 | func (cmds orderedCommands) Less(i, j int) bool { 55 | return cmds[i].order < cmds[j].order 56 | } 57 | func (cmds orderedCommands) Swap(i, j int) { 58 | cmds[i], cmds[j] = cmds[j], cmds[i] 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cortana 2 | An extremely easy to use command line parsing library 3 | 4 | # Features 5 | 6 | * Extremely easy to use 7 | * Handling the sub commands exactly the same as the main command 8 | * Parse args from configration file, envrionment and flags 9 | 10 | ## How to use 11 | 12 | ### Work in the main function 13 | 14 | ```go 15 | // say.go 16 | func main() { 17 | args := struct { 18 | Name string `cortana:"--name, -n, tom, who do you want say to"` 19 | Text string `cortana:"text"` 20 | }{} 21 | // Parse the args 22 | cortana.Parse(&args) 23 | fmt.Printf("Say to %s: %s\n", args.Name, args.Text) 24 | } 25 | 26 | $ go run say.go -n alice hello 27 | Say to alice: hello 28 | ``` 29 | 30 | ### Use as the sub-command 31 | 32 | ```go 33 | // pepole.go 34 | func say() { 35 | args := struct { 36 | Name string `cortana:"--name, -n, tom, who do you want say to"` 37 | Text string `cortana:"text"` 38 | }{} 39 | cortana.Parse(&args) 40 | fmt.Printf("Say to %s: %s\n", args.Name, args.Text) 41 | } 42 | 43 | func main() { 44 | // Add "say" as a sub-command 45 | cortana.AddCommand("say", say, "say something") 46 | cortana.Launch() 47 | } 48 | 49 | $ go run pepole.go say -n alice hello 50 | Say to alice: hello 51 | ``` 52 | 53 | ### You defines how the sub-commond looks like without affecting the original implementation 54 | 55 | ```go 56 | // people.go 57 | func main() { 58 | // say greeting works by calling the say function 59 | cortana.AddCommand("say greeting", say, "say something") 60 | // greeting say also works by calling the say function 61 | cortana.AddCommand("greeting say", say, "say something") 62 | cortana.Launch() 63 | } 64 | 65 | $ go run pepole.go say greeting -n alice hello 66 | Say to alice: hello 67 | 68 | $ go run pepole.go greeting say -n alice hello 69 | Say to alice: hello 70 | ``` -------------------------------------------------------------------------------- /example/say.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/shafreeck/cortana" 8 | ) 9 | 10 | func sayHelloCortana() { 11 | fmt.Println("hello cortana") 12 | } 13 | func sayHelloAnyone() { 14 | person := struct { 15 | Name string `cortana:"name"` 16 | }{} 17 | cortana.Parse(&person) 18 | 19 | fmt.Println("hello", person.Name) 20 | } 21 | func sayAnything() { 22 | cortana.Title("Say anything to anyone") 23 | cortana.Description(`You can say anything you want to anyone, the person can be 24 | selected by using the name, age or location. You can even combine these conditions 25 | together to choose the person more effectively`) 26 | greeting := struct { //format: --long -short defaultValue description 27 | Name string `lsdd:"--name, -n, cortana, say something to cortana"` 28 | Age int `cortana:"--age, -, 18, say something to someone with certain age"` 29 | Location string `cortana:"--location, -l, beijing, say something to someone lives in certain location"` 30 | Text string `cortana:"text, -, -"` 31 | }{} 32 | cortana.Parse(&greeting) 33 | 34 | fmt.Printf("Say to %s who is %d year old and lives in %s now:\n", 35 | greeting.Name, greeting.Age, greeting.Location) 36 | fmt.Println(greeting.Text) 37 | 38 | } 39 | 40 | func complete() { 41 | cortana.Title("Complete a command") 42 | cortana.Description("return all the commands that has prefix") 43 | opts := struct { 44 | Prefix string `cortana:"prefix"` 45 | }{} 46 | cortana.Parse(&opts) 47 | 48 | cmds := cortana.Complete(opts.Prefix) 49 | for _, cmd := range cmds { 50 | fmt.Println(cmd.Path+":", cmd.Brief) 51 | } 52 | } 53 | 54 | func main() { 55 | cortana.AddCommand("say hello cortana", sayHelloCortana, "say hello to cortana") 56 | cortana.AddCommand("say hello", sayHelloAnyone, "say hello to anyone") 57 | cortana.AddCommand("say", sayAnything, "say anything to anyone") 58 | cortana.AddCommand("complete", complete, "complete a command prefix") 59 | 60 | cortana.Alias("cortana", "say hello cortana") 61 | 62 | cortana.AddConfig("greeting.json", cortana.UnmarshalFunc(json.Unmarshal)) 63 | cortana.Use(cortana.ConfFlag("--config", "-c", cortana.UnmarshalFunc(json.Unmarshal))) 64 | 65 | cortana.Launch() 66 | } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /cortana.go: -------------------------------------------------------------------------------- 1 | package cortana 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "reflect" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | "time" 15 | "unicode" 16 | "unsafe" 17 | 18 | "github.com/google/btree" 19 | "github.com/muesli/reflow/wordwrap" 20 | ) 21 | 22 | type predefined struct { 23 | help longshort 24 | cfg struct { 25 | longshort 26 | unmarshaler Unmarshaler 27 | } 28 | } 29 | 30 | // Cortana is the commander 31 | type Cortana struct { 32 | ctx context 33 | commands commands 34 | predefined predefined 35 | configs []*config 36 | envs []EnvUnmarshaler 37 | stdout io.Writer 38 | stderr io.Writer 39 | exitOnErr bool 40 | 41 | parsing struct { 42 | flags []*flag 43 | nonflags []*nonflag 44 | } 45 | 46 | // seq keeps the order of adding a command 47 | seq int 48 | } 49 | 50 | type Option func(c *Cortana) 51 | 52 | func HelpFlag(long, short string) Option { 53 | return func(c *Cortana) { 54 | c.predefined.help.long = long 55 | c.predefined.help.short = short 56 | c.predefined.help.desc = "help for the command" 57 | } 58 | } 59 | func DisableHelpFlag() Option { 60 | return HelpFlag("", "") 61 | } 62 | 63 | func WithStdout(stdout io.Writer) Option { 64 | return func(c *Cortana) { 65 | c.stdout = stdout 66 | } 67 | } 68 | 69 | func WithStderr(stderr io.Writer) Option { 70 | return func(c *Cortana) { 71 | c.stdout = stderr 72 | } 73 | } 74 | 75 | func ExitOnError(b bool) Option { 76 | return func(c *Cortana) { 77 | c.exitOnErr = b 78 | } 79 | } 80 | 81 | // ConfFlag parse the configration file path from flags 82 | func ConfFlag(long, short string, unmarshaler Unmarshaler) Option { 83 | return func(c *Cortana) { 84 | c.predefined.cfg.long = long 85 | c.predefined.cfg.short = short 86 | c.predefined.cfg.desc = "path of the configuration file" 87 | c.predefined.cfg.unmarshaler = unmarshaler 88 | } 89 | } 90 | 91 | // New a Cortana commander 92 | func New(opts ...Option) *Cortana { 93 | c := &Cortana{commands: commands{t: btree.New(8)}, 94 | ctx: context{args: os.Args[1:], name: os.Args[0]}, 95 | stdout: os.Stdout, 96 | stderr: os.Stderr, 97 | exitOnErr: true, 98 | } 99 | c.predefined.help = longshort{ 100 | long: "--help", 101 | short: "-h", 102 | desc: "help for the command", 103 | } 104 | for _, opt := range opts { 105 | opt(c) 106 | } 107 | return c 108 | } 109 | 110 | // fatal exit the process with an error 111 | func (c *Cortana) fatal(err error) { 112 | fmt.Fprintln(c.stderr, err) 113 | if c.exitOnErr { 114 | os.Exit(-1) 115 | } 116 | } 117 | 118 | // Use the cortana options 119 | func (c *Cortana) Use(opts ...Option) { 120 | for _, opt := range opts { 121 | opt(c) 122 | } 123 | } 124 | 125 | // AddCommand adds a command 126 | func (c *Cortana) AddCommand(path string, cmd func(), brief string) { 127 | c.commands.t.ReplaceOrInsert(&command{Path: path, Proc: cmd, Brief: brief, order: c.seq}) 128 | c.seq++ 129 | } 130 | 131 | // AddRootCommand adds the command without sub path 132 | func (c *Cortana) AddRootCommand(cmd func()) { 133 | c.AddCommand("", cmd, "") 134 | } 135 | 136 | // AddConfig adds a config file 137 | func (c *Cortana) AddConfig(path string, unmarshaler Unmarshaler) { 138 | // expand the path 139 | if path != "" && path[0] == '~' { 140 | home, _ := os.UserHomeDir() 141 | if home != "" { 142 | path = home + path[1:] 143 | } 144 | } 145 | cfg := &config{path: path, unmarshaler: unmarshaler} 146 | c.configs = append(c.configs, cfg) 147 | } 148 | 149 | func (c *Cortana) AddEnvUnmarshaler(unmarshaler EnvUnmarshaler) { 150 | c.envs = append(c.envs, unmarshaler) 151 | } 152 | 153 | // Launch and run commands, os.Args is used if no args supplied 154 | func (c *Cortana) Launch(args ...string) { 155 | if len(args) == 0 { 156 | args = os.Args[1:] 157 | } 158 | cmd := c.SearchCommand(args) 159 | if cmd == nil { 160 | c.Usage() 161 | if len(args) > 0 && !strings.HasPrefix(args[0], "-") { 162 | c.fatal(errors.New("unknown command: " + args[0])) 163 | } 164 | return 165 | } 166 | cmd.Proc() 167 | } 168 | 169 | // SearchCommand returns the command according the args 170 | func (c *Cortana) SearchCommand(args []string) *Command { 171 | var cmdArgs []string 172 | var maybeArgs []string 173 | var path string 174 | const ( 175 | StateCommand = iota 176 | StateCommandPrefix 177 | StateOptionFlag 178 | StateOptionArg 179 | StateCommandArg 180 | ) 181 | 182 | // reset the search context 183 | c.ctx = context{} 184 | 185 | st := StateCommand 186 | cmd := c.commands.get(path) 187 | for i := 0; i < len(args); i++ { 188 | arg := args[i] 189 | switch st { 190 | case StateCommand: 191 | if strings.HasPrefix(arg, "-") { 192 | st = StateOptionFlag 193 | cmdArgs = append(cmdArgs, arg) 194 | continue 195 | } 196 | p := strings.TrimSpace(path + " " + arg) 197 | commands := c.commands.scan(p) 198 | if len(commands) > 0 { 199 | path = p 200 | if commands[0].Path == path { 201 | maybeArgs = maybeArgs[:0] 202 | cmd = commands[0] 203 | st = StateCommand 204 | continue 205 | } 206 | maybeArgs = append(maybeArgs, arg) 207 | st = StateCommandPrefix 208 | continue 209 | } 210 | if cmd != nil { 211 | cmdArgs = append(cmdArgs, arg) 212 | st = StateCommandArg 213 | continue 214 | } 215 | return nil 216 | 217 | case StateCommandPrefix: 218 | if strings.HasPrefix(arg, "-") { 219 | st = StateOptionFlag 220 | cmdArgs = append(cmdArgs, arg) 221 | continue 222 | } 223 | 224 | p := strings.TrimSpace(path + " " + arg) 225 | commands := c.commands.scan(p) 226 | if len(commands) > 0 { 227 | path = p 228 | if commands[0].Path == path { 229 | maybeArgs = maybeArgs[:0] 230 | cmd = commands[0] 231 | st = StateCommand 232 | continue 233 | } 234 | continue 235 | } 236 | 237 | case StateOptionFlag: 238 | if strings.HasPrefix(arg, "-") { 239 | cmdArgs = append(cmdArgs, arg) 240 | continue 241 | } 242 | 243 | p := strings.TrimSpace(path + " " + args[i]) 244 | commands := c.commands.scan(p) 245 | if len(commands) > 0 { 246 | path = p 247 | if commands[0].Path == path { 248 | maybeArgs = maybeArgs[:0] 249 | cmd = commands[0] 250 | st = StateCommand 251 | continue 252 | } 253 | maybeArgs = append(maybeArgs, arg) 254 | st = StateCommandPrefix 255 | continue 256 | } 257 | cmdArgs = append(cmdArgs, arg) 258 | st = StateOptionArg 259 | 260 | case StateOptionArg: 261 | if strings.HasPrefix(arg, "-") { 262 | cmdArgs = append(cmdArgs, arg) 263 | st = StateOptionFlag 264 | continue 265 | } 266 | 267 | p := strings.TrimSpace(path + " " + args[i]) 268 | commands := c.commands.scan(p) 269 | if len(commands) > 0 { 270 | path = p 271 | if commands[0].Path == path { 272 | maybeArgs = maybeArgs[:0] 273 | cmd = commands[0] 274 | st = StateCommand 275 | continue 276 | } 277 | maybeArgs = append(maybeArgs, arg) 278 | st = StateCommandPrefix 279 | continue 280 | } 281 | cmdArgs = append(cmdArgs, arg) 282 | st = StateCommandArg 283 | 284 | case StateCommandArg: 285 | if strings.HasPrefix(arg, "-") { 286 | cmdArgs = append(cmdArgs, arg) 287 | st = StateOptionFlag 288 | continue 289 | } 290 | cmdArgs = append(cmdArgs, arg) 291 | } 292 | } 293 | 294 | cmdArgs = append(cmdArgs, maybeArgs...) 295 | name := path 296 | if cmd != nil { 297 | name = cmd.Path 298 | } 299 | c.ctx = context{ 300 | name: name, 301 | args: cmdArgs, 302 | longest: path, 303 | } 304 | return (*Command)(cmd) 305 | } 306 | 307 | // Args returns the args in current context 308 | func (c *Cortana) Args() []string { 309 | return c.ctx.args 310 | } 311 | 312 | // Commands returns all the available commands 313 | func (c *Cortana) Commands() []*Command { 314 | var commands []*Command 315 | 316 | // scan all the commands 317 | cmds := c.commands.scan("") 318 | for _, c := range cmds { 319 | commands = append(commands, (*Command)(c)) 320 | } 321 | return commands 322 | } 323 | 324 | type parseOption struct { 325 | ignoreUnknownArgs bool 326 | args []string 327 | onUsage func(usage string) // a callback after parsing "--help, -h" 328 | } 329 | type ParseOption func(opt *parseOption) 330 | 331 | func IgnoreUnknownArgs() ParseOption { 332 | return func(opt *parseOption) { 333 | opt.ignoreUnknownArgs = true 334 | } 335 | } 336 | 337 | func WithArgs(args []string) ParseOption { 338 | return func(opt *parseOption) { 339 | opt.args = args 340 | } 341 | } 342 | 343 | func OnUsage(f func(usage string)) ParseOption { 344 | return func(opt *parseOption) { 345 | opt.onUsage = f 346 | } 347 | } 348 | 349 | // Parse the flags 350 | func (c *Cortana) Parse(v interface{}, opts ...ParseOption) { 351 | if v == nil { 352 | return 353 | } 354 | // print the usage and exit by default when parsing the usage/help flags 355 | opt := parseOption{onUsage: func(usage string) { 356 | fmt.Fprint(c.stdout, usage) 357 | os.Exit(0) 358 | }} 359 | for _, o := range opts { 360 | o(&opt) 361 | } 362 | if opt.args != nil { 363 | c.ctx.args = opt.args 364 | } 365 | 366 | // process the defined args 367 | c.parsing.flags = nil // reset parsing state, so the Parse function could be reused 368 | c.parsing.nonflags = nil 369 | flags, nonflags := parseCortanaTags(reflect.ValueOf(v)) 370 | c.parsing.flags = append(c.parsing.flags, flags...) 371 | c.parsing.nonflags = append(c.parsing.nonflags, nonflags...) 372 | c.collectFlags() 373 | c.applyDefaultValues() 374 | 375 | for func() (restart bool) { 376 | defer func() { 377 | if v := recover(); v != nil { 378 | if s, ok := v.(string); ok && s == "restart" { 379 | restart = true 380 | } else if s == "abort" { 381 | return 382 | } else { 383 | panic(v) 384 | } 385 | } 386 | }() 387 | c.unmarshalConfigs(v) 388 | c.unmarshalEnvs(v) 389 | c.unmarshalArgs(opt.ignoreUnknownArgs, opt.onUsage) 390 | c.checkRequires() 391 | return false 392 | }() { 393 | } 394 | } 395 | 396 | // Title set the title for the command 397 | func (c *Cortana) Title(text string) { 398 | c.ctx.desc.title = text 399 | } 400 | 401 | // Description set the description for the command, it always be helpful 402 | // to describe about the details of command 403 | func (c *Cortana) Description(text string) { 404 | c.ctx.desc.description = text 405 | } 406 | 407 | // Usage prints the usage 408 | func (c *Cortana) Usage() { 409 | fmt.Fprint(c.stdout, c.UsageString()) 410 | } 411 | 412 | // Usage returns the usage string 413 | func (c *Cortana) UsageString() string { 414 | out := bytes.NewBuffer(nil) 415 | if c.ctx.desc.title != "" { 416 | out.WriteString(c.ctx.desc.title + "\n\n") 417 | } 418 | if c.ctx.desc.description != "" { 419 | out.WriteString(c.ctx.desc.description + "\n\n") 420 | } 421 | 422 | // print the aliailable commands 423 | commands := c.commands.scan(c.ctx.longest) 424 | // ignore the command itself 425 | if len(commands) > 0 && commands[0].Path == c.ctx.name { 426 | commands = commands[1:] 427 | } 428 | if len(commands) > 0 { 429 | out.WriteString("Available commands:\n\n") 430 | sort.Sort(orderedCommands(commands)) 431 | 432 | cmds := bytes.NewBuffer(nil) 433 | alias := bytes.NewBuffer(nil) 434 | for _, cmd := range commands { 435 | writeString := cmds.WriteString 436 | if cmd.Alias { 437 | writeString = alias.WriteString 438 | } 439 | writeString(fmt.Sprintf("%-30s%s\n", cmd.Path, cmd.Brief)) 440 | } 441 | out.WriteString(cmds.String() + "\n\n") 442 | if alias.Len() > 0 { 443 | out.WriteString("Alias commands:\n\n") 444 | out.WriteString(alias.String() + "\n") 445 | } 446 | } 447 | 448 | if c.ctx.desc.flags != "" { 449 | out.WriteString("Usage:" + c.ctx.desc.flags + "\n") 450 | } 451 | return out.String() 452 | } 453 | 454 | // Complete returns all the commands that has prefix 455 | func (c *Cortana) Complete(prefix string) []*Command { 456 | cmds := c.commands.scan(prefix) 457 | return *(*[]*Command)(unsafe.Pointer(&cmds)) 458 | } 459 | 460 | func (c *Cortana) Alias(name, definition string) { 461 | processAlias := func() { 462 | c.alias(definition) 463 | } 464 | alias := fmt.Sprintf("alias %-5s = %-20s", name, definition) 465 | c.commands.t.ReplaceOrInsert(&command{Path: name, Proc: processAlias, Brief: alias, order: c.seq, Alias: true}) 466 | c.seq++ 467 | } 468 | func (c *Cortana) alias(definition string) { 469 | quoted := false 470 | args := strings.FieldsFunc(definition, func(r rune) bool { 471 | if r == '"' { 472 | quoted = !quoted 473 | } 474 | return unicode.IsSpace(r) && !quoted 475 | }) 476 | cmd := c.SearchCommand(append(args, c.ctx.args...)) 477 | if cmd == nil { 478 | c.Usage() 479 | return 480 | } 481 | cmd.Proc() 482 | } 483 | 484 | func (c *Cortana) collectFlags() { 485 | flags, nonflags := c.parsing.flags, c.parsing.nonflags 486 | 487 | w := bytes.NewBuffer(nil) 488 | w.WriteString(c.ctx.name) 489 | if len(flags) > 0 { 490 | w.WriteString(" [options]") 491 | } 492 | for _, nf := range nonflags { 493 | name := nf.long 494 | if name == "" { 495 | name = nf.name 496 | } 497 | if nf.rv.Kind() == reflect.Slice { 498 | name += "..." 499 | } 500 | if nf.required { 501 | w.WriteString(" <" + name + ">") 502 | } else { 503 | w.WriteString(" [" + name + "]") 504 | } 505 | } 506 | w.WriteString("\n\n") 507 | 508 | if c.predefined.help.short != "" || c.predefined.help.long != "" { 509 | flags = append(flags, &flag{ 510 | long: c.predefined.help.long, 511 | short: c.predefined.help.short, 512 | description: c.predefined.help.desc, 513 | rv: reflect.ValueOf(false), 514 | }) 515 | } 516 | if c.predefined.cfg.short != "" || c.predefined.cfg.long != "" { 517 | path := "" 518 | for i, cfg := range c.configs { 519 | if i == len(c.configs)-1 { 520 | path += cfg.path 521 | } else { 522 | path += cfg.path + "," 523 | } 524 | } 525 | flags = append(flags, &flag{ 526 | long: c.predefined.cfg.long, 527 | short: c.predefined.cfg.short, 528 | description: c.predefined.cfg.desc, 529 | required: true, 530 | defaultValue: path, 531 | }) 532 | c.configs = append(c.configs, &config{ 533 | path: "", // this should be determined by parsing the args 534 | unmarshaler: c.predefined.cfg.unmarshaler, 535 | }) 536 | } 537 | for _, f := range flags { 538 | var flag string 539 | if f.short != "-" && f.short != "" { 540 | flag += f.short 541 | } 542 | if f.long != "-" { 543 | if f.short != "-" && f.short != "" { 544 | flag += ", " + f.long 545 | } else { 546 | flag += " " + f.long 547 | } 548 | } 549 | if f.rv.Kind() != reflect.Bool { 550 | if f.long != "-" { 551 | flag += " <" + strings.TrimLeft(f.long, "-") + ">" 552 | } else { 553 | flag += " <" + strings.ToLower(f.name) + ">" 554 | } 555 | } 556 | if len(flag) > 30 { 557 | // align with 32 spaces 558 | flag += "\n " 559 | } 560 | if !f.required && f.rv.Kind() != reflect.Bool { 561 | s := wordWrapWithPrefix(fmt.Sprintf(" %-30s ", flag), f.description, 50, 33) // 30+ 3 spaces 562 | defaultValue := fmt.Sprintf("(default=%s)\n", f.defaultValue) 563 | // if no default value, use its zero value 564 | if f.defaultValue == "" { 565 | defaultValue = fmt.Sprintf("(default=%v)\n", f.rv.Interface()) 566 | if f.rv.Kind() == reflect.String { 567 | defaultValue = fmt.Sprintf("(default=%q)\n", f.rv.Interface()) 568 | } 569 | } 570 | w.WriteString(s + defaultValue) 571 | } else { 572 | s := wordWrapWithPrefix(fmt.Sprintf(" %-30s ", flag), f.description, 50, 33) 573 | w.WriteString(s + "\n") 574 | } 575 | } 576 | 577 | c.ctx.desc.flags = w.String() 578 | } 579 | 580 | func parseCortanaTags(rv reflect.Value) ([]*flag, []*nonflag) { 581 | flags := make([]*flag, 0) 582 | nonflags := make([]*nonflag, 0) 583 | for rv.Kind() == reflect.Ptr { 584 | rv = rv.Elem() 585 | } 586 | 587 | rt := rv.Type() 588 | for i := 0; i < rt.NumField(); i++ { 589 | ft := rt.Field(i) 590 | fv := rv.Field(i) 591 | if fv.Kind() == reflect.Struct { 592 | f, nf := parseCortanaTags(fv) 593 | flags = append(flags, f...) 594 | nonflags = append(nonflags, nf...) 595 | continue 596 | } 597 | 598 | tag := ft.Tag.Get("cortana") 599 | if tag == "" { 600 | tag = ft.Tag.Get("lsdd") // lsdd is short for (long short default description) 601 | } 602 | f := parseFlag(tag, ft.Name, fv) 603 | if strings.HasPrefix(f.long, "-") { 604 | if f.long != "-" || f.short != "-" { 605 | flags = append(flags, f) 606 | } 607 | } else { 608 | nf := nonflag(*f) 609 | nonflags = append(nonflags, &nf) 610 | } 611 | } 612 | return flags, nonflags 613 | } 614 | func buildArgsIndex(flags []*flag) map[string]*flag { 615 | flagsIdx := make(map[string]*flag) 616 | for _, f := range flags { 617 | if f.long != "" { 618 | flagsIdx[f.long] = f 619 | } 620 | if f.short != "" { 621 | flagsIdx[f.short] = f 622 | } 623 | } 624 | return flagsIdx 625 | } 626 | func (c *Cortana) applyDefaultValues() { 627 | for _, nf := range c.parsing.nonflags { 628 | if nf.required { 629 | continue 630 | } 631 | if err := applyValue(nf.rv, nf.defaultValue); err != nil { 632 | c.fatal(err) 633 | } 634 | } 635 | for _, f := range c.parsing.flags { 636 | if f.required { 637 | continue 638 | } 639 | if f.rv.Kind() == reflect.Slice && f.defaultValue == "nil" { 640 | continue 641 | } 642 | if err := applyValue(f.rv, f.defaultValue); err != nil { 643 | c.fatal(err) 644 | } 645 | } 646 | } 647 | func applyValue(v reflect.Value, s string) error { 648 | if s == "" { 649 | return nil 650 | } 651 | switch v.Kind() { 652 | case reflect.String: 653 | v.SetString(s) 654 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: 655 | var i int64 656 | var d time.Duration 657 | var err error 658 | if v.Type() == reflect.TypeOf(time.Duration(0)) { 659 | d, err = time.ParseDuration(s) 660 | i = int64(d) 661 | } else { 662 | i, err = strconv.ParseInt(s, 10, 64) 663 | } 664 | if err != nil { 665 | return err 666 | } 667 | v.SetInt(i) 668 | case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64: 669 | u, err := strconv.ParseUint(s, 10, 64) 670 | if err != nil { 671 | return err 672 | } 673 | v.SetUint(u) 674 | case reflect.Float32, reflect.Float64: 675 | f, err := strconv.ParseFloat(s, 64) 676 | if err != nil { 677 | return err 678 | } 679 | v.SetFloat(f) 680 | case reflect.Bool: 681 | b, err := strconv.ParseBool(s) 682 | if err != nil { 683 | return err 684 | } 685 | v.SetBool(b) 686 | case reflect.Slice: 687 | e := reflect.New(v.Type().Elem()).Elem() 688 | if err := applyValue(e, s); err != nil { 689 | return err 690 | } 691 | v.Set(reflect.Append(v, e)) 692 | } 693 | return nil 694 | } 695 | func (c *Cortana) checkRequires() { 696 | flags, nonflags := c.parsing.flags, c.parsing.nonflags 697 | 698 | args := c.ctx.args 699 | // check the nonflags 700 | i := 0 701 | for _, arg := range args { 702 | if strings.HasPrefix(arg, "-") { 703 | break 704 | } 705 | i++ 706 | } 707 | if i < len(nonflags) { 708 | for _, nf := range nonflags[i:] { 709 | if nf.required && nf.rv.IsZero() { 710 | c.fatal(errors.New("<" + nf.long + "> is required")) 711 | } 712 | } 713 | 714 | } 715 | 716 | // check the flags 717 | argsIdx := make(map[string]struct{}) 718 | for _, arg := range args { 719 | argsIdx[arg] = struct{}{} 720 | } 721 | for _, f := range flags { 722 | if !f.required { 723 | continue 724 | } 725 | if _, ok := argsIdx[f.long]; ok { 726 | continue 727 | } 728 | if _, ok := argsIdx[f.short]; ok { 729 | continue 730 | } 731 | if !f.rv.IsZero() { 732 | continue 733 | } 734 | 735 | if f.long != "-" { 736 | c.fatal(errors.New(f.long + " is required")) 737 | } 738 | if f.short != "-" { 739 | c.fatal(errors.New(f.short + " is required")) 740 | } 741 | } 742 | } 743 | 744 | // unmarshalArgs fills v with the parsed args 745 | func (c *Cortana) unmarshalArgs(ignoreUnknown bool, onUsage func(usage string)) { 746 | flags := buildArgsIndex(c.parsing.flags) 747 | nonflags := c.parsing.nonflags 748 | 749 | var unknown []string 750 | args := c.ctx.args 751 | for i := 0; i < len(args); i++ { 752 | // print the usage and abort 753 | if args[i] == c.predefined.help.long || args[i] == c.predefined.help.short { 754 | onUsage(c.UsageString()) 755 | panic("abort") 756 | } 757 | // handle nonflags 758 | if !strings.HasPrefix(args[i], "-") && len(nonflags) > 0 { 759 | rv := nonflags[0].rv 760 | if err := applyValue(rv, args[i]); err != nil { 761 | c.fatal(err) 762 | } 763 | if rv.Kind() != reflect.Slice { 764 | nonflags = nonflags[1:] 765 | } 766 | continue 767 | } 768 | 769 | var emptyValue bool 770 | var key, value string 771 | if strings.Index(args[i], "=") > 0 { 772 | kvs := strings.SplitN(args[i], "=", 2) 773 | key, value = kvs[0], kvs[1] 774 | // In case of --flag=, user set the flag as an empty value explicitly, the empty value should be allowd 775 | if value == "" { 776 | emptyValue = true 777 | } 778 | } else { 779 | key = args[i] 780 | } 781 | 782 | // handle the config flags 783 | if key == c.predefined.cfg.long || key == c.predefined.cfg.short { 784 | cfg := c.configs[len(c.configs)-1] // overwrite the last one 785 | cfg.requireExist = true 786 | if value != "" { 787 | cfg.path = value 788 | c.ctx.args = append(args[0:i], args[i+1:]...) 789 | panic("restart") 790 | } else if i+1 < len(args) { 791 | next := args[i+1] 792 | if next[0] != '-' { 793 | cfg.path = args[i+1] 794 | c.ctx.args = append(args[0:i], args[i+2:]...) 795 | panic("restart") 796 | } 797 | } 798 | c.fatal(errors.New(key + " requires an argument")) 799 | } 800 | 801 | flag, ok := flags[key] 802 | if ok { 803 | if emptyValue { 804 | continue 805 | } 806 | if value != "" { 807 | if err := applyValue(flag.rv, value); err != nil { 808 | c.fatal(err) 809 | } 810 | continue 811 | } 812 | if flag.rv.Kind() == reflect.Bool { 813 | if err := applyValue(flag.rv, "true"); err != nil { 814 | c.fatal(err) 815 | } 816 | continue 817 | } 818 | if i+1 < len(args) { 819 | next := args[i+1] 820 | if next[0] != '-' || next == "--" { // allow "--" as a special value 821 | if err := applyValue(flag.rv, next); err != nil { 822 | c.fatal(err) 823 | } 824 | i++ 825 | continue 826 | } 827 | } 828 | c.fatal(errors.New(key + " requires an argument")) 829 | } else { 830 | if ignoreUnknown { 831 | unknown = append(unknown, args[i]) 832 | } else { 833 | c.fatal(errors.New("unknown argument: " + args[i])) 834 | } 835 | } 836 | } 837 | c.ctx.args = unknown 838 | } 839 | 840 | func (c *Cortana) unmarshalConfigs(v interface{}) { 841 | for _, cfg := range c.configs { 842 | file, err := os.Open(cfg.path) 843 | if err != nil { 844 | if os.IsNotExist(err) && !cfg.requireExist { 845 | continue 846 | } 847 | c.fatal(err) 848 | } 849 | data, err := ioutil.ReadAll(file) 850 | if err != nil { 851 | c.fatal(err) 852 | } 853 | 854 | if err := cfg.unmarshaler.Unmarshal(data, v); err != nil { 855 | c.fatal(err) 856 | } 857 | file.Close() 858 | } 859 | } 860 | 861 | func (c *Cortana) unmarshalEnvs(v interface{}) { 862 | for _, u := range c.envs { 863 | if err := u.Unmarshal(v); err != nil { 864 | c.fatal(err) 865 | } 866 | } 867 | } 868 | 869 | // 870 | // prefix: one 871 | // two 872 | // three 873 | // 874 | 875 | // wrap text with prefix as format above 876 | func wordWrapWithPrefix(prefix string, text string, width, indent int) string { 877 | lines := strings.Split(wordwrap.String(text, width), "\n") 878 | 879 | if len(lines) == 0 { 880 | return prefix 881 | } 882 | 883 | b := &strings.Builder{} 884 | b.WriteString(prefix) 885 | b.WriteString(lines[0] + "\n") 886 | for i := 1; i < len(lines); i++ { 887 | align := strings.Repeat(" ", indent) 888 | b.WriteString(align + lines[i] + "\n") 889 | } 890 | return strings.TrimRight(b.String(), "\n") 891 | } 892 | 893 | var c *Cortana 894 | 895 | func init() { 896 | c = New() 897 | } 898 | 899 | // Parse the arguemnts into a struct 900 | func Parse(v interface{}, opts ...ParseOption) { 901 | c.Parse(v, opts...) 902 | } 903 | 904 | // Title set the title for the command 905 | func Title(text string) { 906 | c.Title(text) 907 | } 908 | 909 | // Description set the description for the command, it always be helpful 910 | // to describe about the details of command 911 | func Description(text string) { 912 | c.Description(text) 913 | } 914 | 915 | // Usage prints the usage and exits 916 | func Usage() { 917 | c.Usage() 918 | } 919 | 920 | // Alias gives another name for command. Ex. cortana.Alias("rmi", "rm -i") 921 | func Alias(name, definition string) { 922 | c.Alias(name, definition) 923 | } 924 | 925 | // Args returns the arguments for current command 926 | func Args() []string { 927 | return c.Args() 928 | } 929 | 930 | // AddCommand adds a command 931 | func AddCommand(path string, cmd func(), brief string) { 932 | c.AddCommand(path, cmd, brief) 933 | } 934 | 935 | // AddRootCommand adds the command without sub path 936 | func AddRootCommand(cmd func()) { 937 | c.AddRootCommand(cmd) 938 | } 939 | 940 | // AddConfig adds a configuration file 941 | func AddConfig(path string, unmarshaler Unmarshaler) { 942 | c.AddConfig(path, unmarshaler) 943 | } 944 | 945 | // Commands returns the list of the added commands 946 | func Commands() []*Command { 947 | return c.Commands() 948 | } 949 | 950 | // Launch finds and executes the command, os.Args is used if no args supplied 951 | func Launch(args ...string) { 952 | c.Launch(args...) 953 | } 954 | 955 | // Use the cortana options 956 | func Use(opts ...Option) { 957 | c.Use(opts...) 958 | } 959 | 960 | // Complete returns all the commands that has prefix 961 | func Complete(prefix string) []*Command { 962 | return c.Complete(prefix) 963 | } 964 | 965 | // SearchCommand returns the command according the args 966 | func SearchCommand(args []string) *Command { 967 | return c.SearchCommand(args) 968 | } 969 | 970 | // Usage returns the usage string 971 | func UsageString() string { 972 | return c.UsageString() 973 | } 974 | --------------------------------------------------------------------------------