├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── app.go ├── argmap.go ├── argopt.go ├── args.go ├── command.go ├── commands.go ├── completer.go ├── config.go ├── context.go ├── flagmap.go ├── flags.go ├── functions.go ├── go.mod ├── go.sum ├── grumble.go └── sample ├── full ├── cmd │ ├── admin.go │ ├── app.go │ ├── args.go │ ├── ask.go │ ├── daemon.go │ ├── flags.go │ └── prompt.go └── main.go ├── readline ├── README.md ├── cli │ └── main.go ├── cmd │ └── app.go └── main.go └── simple ├── cmd └── app.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /sample/full/full 3 | /sample/simple/simple 4 | .idea 5 | .vscode 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Roland Singer 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 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 | # Grumble - A powerful modern CLI and SHELL 2 | 3 | [![GoDoc](https://godoc.org/github.com/desertbit/grumble?status.svg)](https://godoc.org/github.com/desertbit/grumble) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/desertbit/grumble)](https://goreportcard.com/report/github.com/desertbit/grumble) 5 | 6 | There are a handful of powerful go CLI libraries available ([spf13/cobra](https://github.com/spf13/cobra), [urfave/cli](https://github.com/urfave/cli)). 7 | However sometimes an integrated shell interface is a great and useful extension for the actual application. 8 | This library offers a simple API to create powerful CLI applications and automatically starts 9 | an **integrated interactive shell**, if the application is started without any command arguments. 10 | 11 | **Hint:** We do not guarantee 100% backwards compatiblity between minor versions (1.x). However, the API is mostly stable and should not change much. 12 | 13 | [![asciicast](https://asciinema.org/a/155332.png)](https://asciinema.org/a/155332?t=5) 14 | 15 | ## Introduction 16 | 17 | Create a grumble APP. 18 | 19 | ```go 20 | var app = grumble.New(&grumble.Config{ 21 | Name: "app", 22 | Description: "short app description", 23 | 24 | Flags: func(f *grumble.Flags) { 25 | f.String("d", "directory", "DEFAULT", "set an alternative directory path") 26 | f.Bool("v", "verbose", false, "enable verbose mode") 27 | }, 28 | }) 29 | ``` 30 | 31 | Register a top-level command. *Note: Sub commands are also supported...* 32 | 33 | ```go 34 | app.AddCommand(&grumble.Command{ 35 | Name: "daemon", 36 | Help: "run the daemon", 37 | Aliases: []string{"run"}, 38 | 39 | Flags: func(f *grumble.Flags) { 40 | f.Duration("t", "timeout", time.Second, "timeout duration") 41 | }, 42 | 43 | Args: func(a *grumble.Args) { 44 | a.String("service", "which service to start", grumble.Default("server")) 45 | }, 46 | 47 | Run: func(c *grumble.Context) error { 48 | // Parent Flags. 49 | c.App.Println("directory:", c.Flags.String("directory")) 50 | c.App.Println("verbose:", c.Flags.Bool("verbose")) 51 | // Flags. 52 | c.App.Println("timeout:", c.Flags.Duration("timeout")) 53 | // Args. 54 | c.App.Println("service:", c.Args.String("service")) 55 | return nil 56 | }, 57 | }) 58 | ``` 59 | 60 | Run the application. 61 | 62 | ```go 63 | err := app.Run() 64 | ``` 65 | 66 | Or use the builtin *grumble.Main* function to handle errors automatically. 67 | 68 | ```go 69 | func main() { 70 | grumble.Main(app) 71 | } 72 | ``` 73 | 74 | ## Shell Multiline Input 75 | 76 | Builtin support for multiple lines. 77 | 78 | ``` 79 | >>> This is \ 80 | ... a multi line \ 81 | ... command 82 | ``` 83 | 84 | ## Separate flags and args specifically 85 | If you need to pass a flag-like value as positional argument, you can do so by using a double dash: 86 | `>>> command --flag1=something -- --myPositionalArg` 87 | 88 | ## Remote shell access with readline 89 | By calling RunWithReadline() rather than Run() you can pass instance of readline.Instance. 90 | One of interesting usages is having a possibility of remote access to your shell: 91 | 92 | ```go 93 | handleFunc := func(rl *readline.Instance) { 94 | 95 | var app = grumble.New(&grumble.Config{ 96 | // override default interrupt handler to avoid remote shutdown 97 | InterruptHandler: func(a *grumble.App, count int) { 98 | // do nothing 99 | }, 100 | 101 | // your usual grumble configuration 102 | }) 103 | 104 | // add commands 105 | 106 | app.RunWithReadline(rl) 107 | 108 | } 109 | 110 | cfg := &readline.Config{} 111 | readline.ListenRemote("tcp", ":5555", cfg, handleFunc) 112 | ``` 113 | 114 | In the client code just use readline built in DialRemote function: 115 | 116 | ```go 117 | if err := readline.DialRemote("tcp", ":5555"); err != nil { 118 | fmt.Errorf("An error occurred: %s \n", err.Error()) 119 | } 120 | ``` 121 | 122 | ## Samples 123 | 124 | Check out the [sample directory](/sample) for some detailed examples. 125 | 126 | ## Projects using Grumble 127 | 128 | - grml - A simple build automation tool written in Go: https://github.com/desertbit/grml 129 | - orbit - A RPC-like networking backend written in Go: https://github.com/desertbit/orbit 130 | 131 | ## Known issues 132 | - Windows unicode not fully supported ([issue](https://github.com/desertbit/grumble/issues/48)) 133 | 134 | ## Additional Useful Packages 135 | 136 | - https://github.com/AlecAivazis/survey 137 | - https://github.com/tj/go-spin 138 | 139 | ## Credits 140 | 141 | This project is based on ideas from the great [ishell](https://github.com/abiosoft/ishell) library. 142 | 143 | ## License 144 | 145 | MIT 146 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "fmt" 29 | "io" 30 | "os" 31 | "strings" 32 | 33 | "github.com/desertbit/closer/v3" 34 | shlex "github.com/desertbit/go-shlex" 35 | "github.com/desertbit/readline" 36 | "github.com/fatih/color" 37 | ) 38 | 39 | // App is the entrypoint. 40 | type App struct { 41 | closer.Closer 42 | 43 | rl *readline.Instance 44 | config *Config 45 | commands Commands 46 | isShell bool 47 | currentPrompt string 48 | 49 | flags Flags 50 | flagMap FlagMap 51 | 52 | args Args 53 | 54 | initHook func(a *App, flags FlagMap) error 55 | shellHook func(a *App) error 56 | 57 | printHelp func(a *App, shell bool) 58 | printCommandHelp func(a *App, cmd *Command, shell bool) 59 | interruptHandler func(a *App, count int) 60 | printASCIILogo func(a *App) 61 | } 62 | 63 | // New creates a new app. 64 | // Panics if the config is invalid. 65 | func New(c *Config) (a *App) { 66 | // Prepare the config. 67 | c.SetDefaults() 68 | err := c.Validate() 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | // APP. 74 | a = &App{ 75 | Closer: closer.New(), 76 | config: c, 77 | currentPrompt: c.prompt(), 78 | flagMap: make(FlagMap), 79 | printHelp: defaultPrintHelp, 80 | printCommandHelp: defaultPrintCommandHelp, 81 | interruptHandler: defaultInterruptHandler, 82 | } 83 | if c.InterruptHandler != nil { 84 | a.interruptHandler = c.InterruptHandler 85 | } 86 | 87 | // Register the builtin flags. 88 | a.flags.Bool("h", "help", false, "display help") 89 | a.flags.BoolL("nocolor", false, "disable color output") 90 | 91 | // Register the user flags, if present. 92 | if c.Flags != nil { 93 | c.Flags(&a.flags) 94 | } 95 | 96 | return 97 | } 98 | 99 | // SetPrompt sets a new prompt. 100 | func (a *App) SetPrompt(p string) { 101 | if !a.config.NoColor { 102 | p = a.config.PromptColor.Sprint(p) 103 | } 104 | a.currentPrompt = p 105 | } 106 | 107 | // SetDefaultPrompt resets the current prompt to the default prompt as 108 | // configured in the config. 109 | func (a *App) SetDefaultPrompt() { 110 | a.currentPrompt = a.config.prompt() 111 | } 112 | 113 | // IsShell indicates, if this is a shell session. 114 | func (a *App) IsShell() bool { 115 | return a.isShell 116 | } 117 | 118 | // Config returns the app's config value. 119 | func (a *App) Config() *Config { 120 | return a.config 121 | } 122 | 123 | // Commands returns the app's commands. 124 | // Access is not thread-safe. Only access during command execution. 125 | func (a *App) Commands() *Commands { 126 | return &a.commands 127 | } 128 | 129 | // PrintError prints the given error. 130 | func (a *App) PrintError(err error) { 131 | if a.config.NoColor { 132 | a.Printf("error: %v\n", err) 133 | } else { 134 | a.config.ErrorColor.Fprint(a, "error: ") 135 | a.Printf("%v\n", err) 136 | } 137 | } 138 | 139 | // Print writes to terminal output. 140 | // Print writes to standard output if terminal output is not yet active. 141 | func (a *App) Print(args ...interface{}) (int, error) { 142 | return fmt.Fprint(a, args...) 143 | } 144 | 145 | // Printf formats according to a format specifier and writes to terminal output. 146 | // Printf writes to standard output if terminal output is not yet active. 147 | func (a *App) Printf(format string, args ...interface{}) (int, error) { 148 | return fmt.Fprintf(a, format, args...) 149 | } 150 | 151 | // Println writes to terminal output followed by a newline. 152 | // Println writes to standard output if terminal output is not yet active. 153 | func (a *App) Println(args ...interface{}) (int, error) { 154 | return fmt.Fprintln(a, args...) 155 | } 156 | 157 | // OnInit sets the function which will be executed before the first command 158 | // is executed. App flags can be handled here. 159 | func (a *App) OnInit(f func(a *App, flags FlagMap) error) { 160 | a.initHook = f 161 | } 162 | 163 | // OnShell sets the function which will be executed before the shell starts. 164 | func (a *App) OnShell(f func(a *App) error) { 165 | a.shellHook = f 166 | } 167 | 168 | // SetInterruptHandler sets the interrupt handler function. 169 | func (a *App) SetInterruptHandler(f func(a *App, count int)) { 170 | a.interruptHandler = f 171 | } 172 | 173 | // SetPrintHelp sets the print help function. 174 | func (a *App) SetPrintHelp(f func(a *App, shell bool)) { 175 | a.printHelp = f 176 | } 177 | 178 | // SetPrintCommandHelp sets the print help function for a single command. 179 | func (a *App) SetPrintCommandHelp(f func(a *App, c *Command, shell bool)) { 180 | a.printCommandHelp = f 181 | } 182 | 183 | // SetPrintASCIILogo sets the function to print the ASCII logo. 184 | func (a *App) SetPrintASCIILogo(f func(a *App)) { 185 | a.printASCIILogo = func(a *App) { 186 | if !a.config.NoColor { 187 | a.config.ASCIILogoColor.Set() 188 | defer color.Unset() 189 | } 190 | f(a) 191 | } 192 | } 193 | 194 | // Write to the underlying output, using readline if available. 195 | func (a *App) Write(p []byte) (int, error) { 196 | return a.Stdout().Write(p) 197 | } 198 | 199 | // Stdout returns a writer to Stdout, using readline if available. 200 | // Note that calling before Run() will return a different instance. 201 | func (a *App) Stdout() io.Writer { 202 | if a.rl != nil { 203 | return a.rl.Stdout() 204 | } 205 | return os.Stdout 206 | } 207 | 208 | // Stderr returns a writer to Stderr, using readline if available. 209 | // Note that calling before Run() will return a different instance. 210 | func (a *App) Stderr() io.Writer { 211 | if a.rl != nil { 212 | return a.rl.Stderr() 213 | } 214 | return os.Stderr 215 | } 216 | 217 | // AddCommand adds a new command. 218 | // Panics on error. 219 | func (a *App) AddCommand(cmd *Command) { 220 | a.addCommand(cmd, true) 221 | } 222 | 223 | // addCommand adds a new command. 224 | // If addHelpFlag is true, a help flag is automatically 225 | // added to the command which displays its usage on use. 226 | // Panics on error. 227 | func (a *App) addCommand(cmd *Command, addHelpFlag bool) { 228 | err := cmd.validate() 229 | if err != nil { 230 | panic(err) 231 | } 232 | cmd.registerFlagsAndArgs(addHelpFlag) 233 | 234 | a.commands.Add(cmd) 235 | } 236 | 237 | // RunCommand runs a single command. 238 | func (a *App) RunCommand(args []string) error { 239 | // Parse the arguments string and obtain the command path to the root, 240 | // and the command flags. 241 | cmds, fg, args, err := a.commands.parse(args, a.flagMap, false) 242 | if err != nil { 243 | return err 244 | } else if len(cmds) == 0 { 245 | return fmt.Errorf("unknown command, try 'help'") 246 | } 247 | 248 | // The last command is the final command. 249 | cmd := cmds[len(cmds)-1] 250 | 251 | // Print the command help if the command run function is nil or if the help flag is set. 252 | if fg.Bool("help") || cmd.Run == nil { 253 | a.printCommandHelp(a, cmd, a.isShell) 254 | return nil 255 | } 256 | 257 | // Parse the arguments. 258 | cmdArgMap := make(ArgMap) 259 | args, err = cmd.args.parse(args, cmdArgMap) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | // Check, if values from the argument string are not consumed (and therefore invalid). 265 | if len(args) > 0 { 266 | return fmt.Errorf("invalid usage of command '%s' (unconsumed input '%s'), try 'help'", cmd.Name, strings.Join(args, " ")) 267 | } 268 | 269 | // Create the context and pass the rest args. 270 | ctx := newContext(a, cmd, fg, cmdArgMap) 271 | 272 | // Run the command. 273 | err = cmd.Run(ctx) 274 | if err != nil { 275 | return err 276 | } 277 | 278 | return nil 279 | } 280 | 281 | // Run the application and parse the command line arguments. 282 | // This method blocks. 283 | func (a *App) Run() (err error) { 284 | // Create the readline instance. 285 | config := &readline.Config{} 286 | a.setReadlineDefaults(config) 287 | rl, err := readline.NewEx(config) 288 | if err != nil { 289 | return err 290 | } 291 | return a.RunWithReadline(rl) 292 | } 293 | 294 | func (a *App) RunWithReadline(rl *readline.Instance) (err error) { 295 | defer a.Close() 296 | 297 | a.setReadlineDefaults(rl.Config) 298 | 299 | // Sort all commands by their name. 300 | a.commands.SortRecursive() 301 | 302 | // Remove the program name from the args. 303 | args := os.Args 304 | if len(args) > 0 { 305 | args = args[1:] 306 | } 307 | 308 | // Parse the app command line flags. 309 | args, err = a.flags.parse(args, a.flagMap) 310 | if err != nil { 311 | return err 312 | } 313 | 314 | // Check if nocolor was set. 315 | a.config.NoColor = a.flagMap.Bool("nocolor") 316 | 317 | // Determine if this is a shell session. 318 | a.isShell = len(args) == 0 319 | 320 | // Add general builtin commands. 321 | a.addCommand(&Command{ 322 | Name: "help", 323 | Help: "use 'help [command]' for command help", 324 | Args: func(a *Args) { 325 | a.StringList("command", "the name of the command") 326 | }, 327 | Run: func(c *Context) error { 328 | args := c.Args.StringList("command") 329 | if len(args) == 0 { 330 | a.printHelp(a, a.isShell) 331 | return nil 332 | } 333 | cmd, _, err := a.commands.FindCommand(args) 334 | if err != nil { 335 | return err 336 | } else if cmd == nil { 337 | a.PrintError(fmt.Errorf("command not found")) 338 | return nil 339 | } 340 | a.printCommandHelp(a, cmd, a.isShell) 341 | return nil 342 | }, 343 | isBuiltin: true, 344 | }, false) 345 | 346 | // Check if help should be displayed. 347 | if a.flagMap.Bool("help") { 348 | a.printHelp(a, false) 349 | return nil 350 | } 351 | 352 | // Add shell builtin commands. 353 | // Ensure to add all commands before running the init hook. 354 | // If the init hook does something with the app commands, then these should also be included. 355 | if a.isShell { 356 | a.AddCommand(&Command{ 357 | Name: "exit", 358 | Help: "exit the shell", 359 | Run: func(c *Context) error { 360 | c.Stop() 361 | return nil 362 | }, 363 | isBuiltin: true, 364 | }) 365 | a.AddCommand(&Command{ 366 | Name: "clear", 367 | Help: "clear the screen", 368 | Run: func(c *Context) error { 369 | readline.ClearScreen(a.rl) 370 | return nil 371 | }, 372 | isBuiltin: true, 373 | }) 374 | } 375 | 376 | // Run the init hook. 377 | if a.initHook != nil { 378 | err = a.initHook(a, a.flagMap) 379 | if err != nil { 380 | return err 381 | } 382 | } 383 | 384 | // Check if a command chould be executed in non-interactive mode. 385 | if !a.isShell { 386 | return a.RunCommand(args) 387 | } 388 | 389 | // Assign readline instance 390 | a.rl = rl 391 | a.OnClose(a.rl.Close) 392 | 393 | // Run the shell hook. 394 | if a.shellHook != nil { 395 | err = a.shellHook(a) 396 | if err != nil { 397 | return err 398 | } 399 | } 400 | 401 | // Print the ASCII logo. 402 | if a.printASCIILogo != nil { 403 | a.printASCIILogo(a) 404 | } 405 | 406 | // Run the shell. 407 | return a.runShell() 408 | } 409 | 410 | func (a *App) setReadlineDefaults(config *readline.Config) { 411 | config.Prompt = a.currentPrompt 412 | config.HistorySearchFold = true 413 | config.DisableAutoSaveHistory = true 414 | config.HistoryFile = a.config.HistoryFile 415 | config.HistoryLimit = a.config.HistoryLimit 416 | config.AutoComplete = newCompleter(&a.commands) 417 | config.VimMode = a.config.VimMode 418 | } 419 | 420 | func (a *App) runShell() error { 421 | var interruptCount int 422 | var lines []string 423 | multiActive := false 424 | 425 | Loop: 426 | for !a.IsClosing() { 427 | // Set the prompt. 428 | if multiActive { 429 | a.rl.SetPrompt(a.config.multiPrompt()) 430 | } else { 431 | a.rl.SetPrompt(a.currentPrompt) 432 | } 433 | multiActive = false 434 | 435 | // Readline. 436 | line, err := a.rl.Readline() 437 | if err != nil { 438 | if err == readline.ErrInterrupt { 439 | interruptCount++ 440 | a.interruptHandler(a, interruptCount) 441 | continue Loop 442 | } else if err == io.EOF { 443 | return nil 444 | } else { 445 | return err 446 | } 447 | } 448 | 449 | // Reset the interrupt count. 450 | interruptCount = 0 451 | 452 | // Handle multiline input. 453 | if strings.HasSuffix(line, "\\") { 454 | multiActive = true 455 | line = strings.TrimSpace(line[:len(line)-1]) // Add without suffix and trim spaces. 456 | lines = append(lines, line) 457 | continue Loop 458 | } 459 | lines = append(lines, strings.TrimSpace(line)) 460 | 461 | line = strings.Join(lines, " ") 462 | line = strings.TrimSpace(line) 463 | lines = lines[:0] 464 | 465 | // Skip if the line is empty. 466 | if len(line) == 0 { 467 | continue Loop 468 | } 469 | 470 | // Save command history. 471 | err = a.rl.SaveHistory(line) 472 | if err != nil { 473 | a.PrintError(err) 474 | continue Loop 475 | } 476 | 477 | // Split the line to args. 478 | args, err := shlex.Split(line, true) 479 | if err != nil { 480 | a.PrintError(fmt.Errorf("invalid args: %v", err)) 481 | continue Loop 482 | } 483 | 484 | // Execute the command. 485 | err = a.RunCommand(args) 486 | if err != nil { 487 | a.PrintError(err) 488 | // Do not continue the Loop here. We want to handle command changes below. 489 | } 490 | 491 | // Sort the commands again if they have changed (Add or remove action). 492 | if a.commands.hasChanged() { 493 | a.commands.SortRecursive() 494 | a.commands.unsetChanged() 495 | } 496 | } 497 | 498 | return nil 499 | } 500 | -------------------------------------------------------------------------------- /argmap.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "fmt" 29 | "time" 30 | ) 31 | 32 | // ArgMapItem holds the specific arg data. 33 | type ArgMapItem struct { 34 | Value interface{} 35 | IsDefault bool 36 | } 37 | 38 | // ArgMap holds all the parsed arg values. 39 | type ArgMap map[string]*ArgMapItem 40 | 41 | // String returns the given arg value as string. 42 | // Panics if not present. Args must be registered. 43 | func (a ArgMap) String(name string) string { 44 | i := a[name] 45 | if i == nil { 46 | panic(fmt.Errorf("missing argument value: arg '%s' not registered", name)) 47 | } 48 | s, ok := i.Value.(string) 49 | if !ok { 50 | panic(fmt.Errorf("failed to assert argument '%s' to string", name)) 51 | } 52 | return s 53 | } 54 | 55 | // StringList returns the given arg value as string slice. 56 | // Panics if not present. Args must be registered. 57 | // If optional and not provided, nil is returned. 58 | func (a ArgMap) StringList(long string) []string { 59 | i := a[long] 60 | if i == nil { 61 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 62 | } 63 | if i.Value == nil { 64 | return nil 65 | } 66 | s, ok := i.Value.([]string) 67 | if !ok { 68 | panic(fmt.Errorf("failed to assert arg '%s' to string list", long)) 69 | } 70 | return s 71 | } 72 | 73 | // Bool returns the given arg value as bool. 74 | // Panics if not present. Args must be registered. 75 | func (a ArgMap) Bool(long string) bool { 76 | i := a[long] 77 | if i == nil { 78 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 79 | } 80 | b, ok := i.Value.(bool) 81 | if !ok { 82 | panic(fmt.Errorf("failed to assert arg '%s' to bool", long)) 83 | } 84 | return b 85 | } 86 | 87 | // BoolList returns the given arg value as bool slice. 88 | // Panics if not present. Args must be registered. 89 | func (a ArgMap) BoolList(long string) []bool { 90 | i := a[long] 91 | if i == nil { 92 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 93 | } 94 | if i.Value == nil { 95 | return nil 96 | } 97 | b, ok := i.Value.([]bool) 98 | if !ok { 99 | panic(fmt.Errorf("failed to assert arg '%s' to bool list", long)) 100 | } 101 | return b 102 | } 103 | 104 | // Int returns the given arg value as int. 105 | // Panics if not present. Args must be registered. 106 | func (a ArgMap) Int(long string) int { 107 | i := a[long] 108 | if i == nil { 109 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 110 | } 111 | v, ok := i.Value.(int) 112 | if !ok { 113 | panic(fmt.Errorf("failed to assert arg '%s' to int", long)) 114 | } 115 | return v 116 | } 117 | 118 | // IntList returns the given arg value as int slice. 119 | // Panics if not present. Args must be registered. 120 | func (a ArgMap) IntList(long string) []int { 121 | i := a[long] 122 | if i == nil { 123 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 124 | } 125 | if i.Value == nil { 126 | return nil 127 | } 128 | v, ok := i.Value.([]int) 129 | if !ok { 130 | panic(fmt.Errorf("failed to assert arg '%s' to int list", long)) 131 | } 132 | return v 133 | } 134 | 135 | // Int64 returns the given arg value as int64. 136 | // Panics if not present. Args must be registered. 137 | func (a ArgMap) Int64(long string) int64 { 138 | i := a[long] 139 | if i == nil { 140 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 141 | } 142 | v, ok := i.Value.(int64) 143 | if !ok { 144 | panic(fmt.Errorf("failed to assert arg '%s' to int64", long)) 145 | } 146 | return v 147 | } 148 | 149 | // Int64List returns the given arg value as int64. 150 | // Panics if not present. Args must be registered. 151 | func (a ArgMap) Int64List(long string) []int64 { 152 | i := a[long] 153 | if i == nil { 154 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 155 | } 156 | if i.Value == nil { 157 | return nil 158 | } 159 | v, ok := i.Value.([]int64) 160 | if !ok { 161 | panic(fmt.Errorf("failed to assert arg '%s' to int64 list", long)) 162 | } 163 | return v 164 | } 165 | 166 | // Uint returns the given arg value as uint. 167 | // Panics if not present. Args must be registered. 168 | func (a ArgMap) Uint(long string) uint { 169 | i := a[long] 170 | if i == nil { 171 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 172 | } 173 | v, ok := i.Value.(uint) 174 | if !ok { 175 | panic(fmt.Errorf("failed to assert arg '%s' to uint", long)) 176 | } 177 | return v 178 | } 179 | 180 | // UintList returns the given arg value as uint. 181 | // Panics if not present. Args must be registered. 182 | func (a ArgMap) UintList(long string) []uint { 183 | i := a[long] 184 | if i == nil { 185 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 186 | } 187 | if i.Value == nil { 188 | return nil 189 | } 190 | v, ok := i.Value.([]uint) 191 | if !ok { 192 | panic(fmt.Errorf("failed to assert arg '%s' to uint list", long)) 193 | } 194 | return v 195 | } 196 | 197 | // Uint64 returns the given arg value as uint64. 198 | // Panics if not present. Args must be registered. 199 | func (a ArgMap) Uint64(long string) uint64 { 200 | i := a[long] 201 | if i == nil { 202 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 203 | } 204 | v, ok := i.Value.(uint64) 205 | if !ok { 206 | panic(fmt.Errorf("failed to assert arg '%s' to uint64", long)) 207 | } 208 | return v 209 | } 210 | 211 | // Uint64List returns the given arg value as uint64. 212 | // Panics if not present. Args must be registered. 213 | func (a ArgMap) Uint64List(long string) []uint64 { 214 | i := a[long] 215 | if i == nil { 216 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 217 | } 218 | if i.Value == nil { 219 | return nil 220 | } 221 | v, ok := i.Value.([]uint64) 222 | if !ok { 223 | panic(fmt.Errorf("failed to assert arg '%s' to uint64 list", long)) 224 | } 225 | return v 226 | } 227 | 228 | // Float64 returns the given arg value as float64. 229 | // Panics if not present. Args must be registered. 230 | func (a ArgMap) Float64(long string) float64 { 231 | i := a[long] 232 | if i == nil { 233 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 234 | } 235 | v, ok := i.Value.(float64) 236 | if !ok { 237 | panic(fmt.Errorf("failed to assert arg '%s' to float64", long)) 238 | } 239 | return v 240 | } 241 | 242 | // Float64List returns the given arg value as float64. 243 | // Panics if not present. Args must be registered. 244 | func (a ArgMap) Float64List(long string) []float64 { 245 | i := a[long] 246 | if i == nil { 247 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 248 | } 249 | if i.Value == nil { 250 | return nil 251 | } 252 | v, ok := i.Value.([]float64) 253 | if !ok { 254 | panic(fmt.Errorf("failed to assert arg '%s' to float64 list", long)) 255 | } 256 | return v 257 | } 258 | 259 | // Duration returns the given arg value as duration. 260 | // Panics if not present. Args must be registered. 261 | func (a ArgMap) Duration(long string) time.Duration { 262 | i := a[long] 263 | if i == nil { 264 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 265 | } 266 | v, ok := i.Value.(time.Duration) 267 | if !ok { 268 | panic(fmt.Errorf("failed to assert arg '%s' to duration", long)) 269 | } 270 | return v 271 | } 272 | 273 | // DurationList returns the given arg value as duration. 274 | // Panics if not present. Args must be registered. 275 | func (a ArgMap) DurationList(long string) []time.Duration { 276 | i := a[long] 277 | if i == nil { 278 | panic(fmt.Errorf("missing arg value: arg '%s' not registered", long)) 279 | } 280 | if i.Value == nil { 281 | return nil 282 | } 283 | v, ok := i.Value.([]time.Duration) 284 | if !ok { 285 | panic(fmt.Errorf("failed to assert arg '%s' to duration list", long)) 286 | } 287 | return v 288 | } 289 | -------------------------------------------------------------------------------- /argopt.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | // ArgOption can be supplied to modify an argument. 28 | type ArgOption func(*argItem) 29 | 30 | // Min sets the minimum required number of elements for a list argument. 31 | func Min(m int) ArgOption { 32 | if m < 0 { 33 | panic("min must be >= 0") 34 | } 35 | 36 | return func(i *argItem) { 37 | if !i.isList { 38 | panic("min option only valid for list arguments") 39 | } 40 | 41 | i.listMin = m 42 | } 43 | } 44 | 45 | // Max sets the maximum required number of elements for a list argument. 46 | func Max(m int) ArgOption { 47 | if m < 1 { 48 | panic("max must be >= 1") 49 | } 50 | 51 | return func(i *argItem) { 52 | if !i.isList { 53 | panic("max option only valid for list arguments") 54 | } 55 | 56 | i.listMax = m 57 | } 58 | } 59 | 60 | // Default sets a default value for the argument. 61 | // The argument becomes optional then. 62 | func Default(v interface{}) ArgOption { 63 | if v == nil { 64 | panic("nil default value not allowed") 65 | } 66 | 67 | return func(i *argItem) { 68 | i.Default = v 69 | i.optional = true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "fmt" 29 | "strconv" 30 | "time" 31 | ) 32 | 33 | // The parseArgFunc describes a func that parses from the given command line arguments 34 | // the values for its argument and saves them to the ArgMap. 35 | // It returns the not-consumed arguments and an error. 36 | type parseArgFunc func(args []string, res ArgMap) ([]string, error) 37 | 38 | type argItem struct { 39 | Name string 40 | Help string 41 | HelpArgs string 42 | Default interface{} 43 | 44 | parser parseArgFunc 45 | isList bool 46 | optional bool 47 | listMin int 48 | listMax int 49 | } 50 | 51 | // Args holds all the registered args. 52 | type Args struct { 53 | list []*argItem 54 | } 55 | 56 | func (a *Args) register( 57 | name, help, helpArgs string, 58 | isList bool, 59 | pf parseArgFunc, 60 | opts ...ArgOption, 61 | ) { 62 | // Validate. 63 | if name == "" { 64 | panic("empty argument name") 65 | } else if help == "" { 66 | panic(fmt.Errorf("missing help message for argument '%s'", name)) 67 | } 68 | 69 | // Ensure the name is unique. 70 | for _, ai := range a.list { 71 | if ai.Name == name { 72 | panic(fmt.Errorf("argument '%s' registered twice", name)) 73 | } 74 | } 75 | 76 | // Create the item. 77 | item := &argItem{ 78 | Name: name, 79 | Help: help, 80 | HelpArgs: helpArgs, 81 | parser: pf, 82 | isList: isList, 83 | optional: isList, 84 | listMin: -1, 85 | listMax: -1, 86 | } 87 | 88 | // Apply options. 89 | // Afterwards, we can make some final checks. 90 | for _, opt := range opts { 91 | opt(item) 92 | } 93 | 94 | if item.isList && item.listMax > 0 && item.listMax < item.listMin { 95 | panic("max must not be less than min for list arguments") 96 | } 97 | 98 | if !a.empty() { 99 | last := a.list[len(a.list)-1] 100 | 101 | // Check, if a list argument has been supplied already. 102 | if last.isList { 103 | panic("list argument has been registered, nothing can come after it") 104 | } 105 | 106 | // Check, that after an optional argument no mandatory one follows. 107 | if !item.optional && last.optional { 108 | panic("mandatory argument not allowed after optional one") 109 | } 110 | } 111 | 112 | a.list = append(a.list, item) 113 | } 114 | 115 | // empty returns true, if the args are empty. 116 | func (a *Args) empty() bool { 117 | return len(a.list) == 0 118 | } 119 | 120 | func (a *Args) parse(args []string, res ArgMap) ([]string, error) { 121 | // Iterate over all arguments that have been registered. 122 | // There must be either a default value or a value available, 123 | // otherwise the argument is missing. 124 | var err error 125 | for _, item := range a.list { 126 | // If it is a list argument, it will consume the rest of the input. 127 | // Check that it matches its range. 128 | if item.isList { 129 | if len(args) < item.listMin { 130 | return nil, fmt.Errorf("argument '%s' requires at least %d element(s)", item.Name, item.listMin) 131 | } 132 | if item.listMax > 0 && len(args) > item.listMax { 133 | return nil, fmt.Errorf("argument '%s' requires at most %d element(s)", item.Name, item.listMax) 134 | } 135 | } 136 | 137 | // If no arguments are left, simply set the default values. 138 | if len(args) == 0 { 139 | // Check, if the argument is mandatory. 140 | if !item.optional { 141 | return nil, fmt.Errorf("missing argument '%s'", item.Name) 142 | } 143 | 144 | // Register its default value. 145 | res[item.Name] = &ArgMapItem{Value: item.Default, IsDefault: true} 146 | continue 147 | } 148 | 149 | args, err = item.parser(args, res) 150 | if err != nil { 151 | return nil, err 152 | } 153 | } 154 | 155 | return args, nil 156 | } 157 | 158 | // String registers a string argument. 159 | func (a *Args) String(name, help string, opts ...ArgOption) { 160 | a.register(name, help, "string", false, 161 | func(args []string, res ArgMap) ([]string, error) { 162 | res[name] = &ArgMapItem{Value: args[0]} 163 | return args[1:], nil 164 | }, 165 | opts..., 166 | ) 167 | } 168 | 169 | // StringList registers a string list argument. 170 | func (a *Args) StringList(name, help string, opts ...ArgOption) { 171 | a.register(name, help, "string list", true, 172 | func(args []string, res ArgMap) ([]string, error) { 173 | res[name] = &ArgMapItem{Value: args} 174 | return []string{}, nil 175 | }, 176 | opts..., 177 | ) 178 | } 179 | 180 | // Bool registers a bool argument. 181 | func (a *Args) Bool(name, help string, opts ...ArgOption) { 182 | a.register(name, help, "bool", false, 183 | func(args []string, res ArgMap) ([]string, error) { 184 | b, err := strconv.ParseBool(args[0]) 185 | if err != nil { 186 | return nil, fmt.Errorf("invalid bool value '%s' for argument: %s", args[0], name) 187 | } 188 | 189 | res[name] = &ArgMapItem{Value: b} 190 | return args[1:], nil 191 | }, 192 | opts..., 193 | ) 194 | } 195 | 196 | // BoolList registers a bool list argument. 197 | func (a *Args) BoolList(name, help string, opts ...ArgOption) { 198 | a.register(name, help, "bool list", true, 199 | func(args []string, res ArgMap) ([]string, error) { 200 | var ( 201 | err error 202 | bs = make([]bool, len(args)) 203 | ) 204 | for i, a := range args { 205 | bs[i], err = strconv.ParseBool(a) 206 | if err != nil { 207 | return nil, fmt.Errorf("invalid bool value '%s' for argument: %s", a, name) 208 | } 209 | } 210 | 211 | res[name] = &ArgMapItem{Value: bs} 212 | return []string{}, nil 213 | }, 214 | opts..., 215 | ) 216 | } 217 | 218 | // Int registers an int argument. 219 | func (a *Args) Int(name, help string, opts ...ArgOption) { 220 | a.register(name, help, "int", false, 221 | func(args []string, res ArgMap) ([]string, error) { 222 | i, err := strconv.Atoi(args[0]) 223 | if err != nil { 224 | return nil, fmt.Errorf("invalid int value '%s' for argument: %s", args[0], name) 225 | } 226 | 227 | res[name] = &ArgMapItem{Value: i} 228 | return args[1:], nil 229 | }, 230 | opts..., 231 | ) 232 | } 233 | 234 | // IntList registers an int list argument. 235 | func (a *Args) IntList(name, help string, opts ...ArgOption) { 236 | a.register(name, help, "int list", true, 237 | func(args []string, res ArgMap) ([]string, error) { 238 | var ( 239 | err error 240 | is = make([]int, len(args)) 241 | ) 242 | for i, a := range args { 243 | is[i], err = strconv.Atoi(a) 244 | if err != nil { 245 | return nil, fmt.Errorf("invalid int value '%s' for argument: %s", a, name) 246 | } 247 | } 248 | 249 | res[name] = &ArgMapItem{Value: is} 250 | return []string{}, nil 251 | }, 252 | opts..., 253 | ) 254 | } 255 | 256 | // Int64 registers an int64 argument. 257 | func (a *Args) Int64(name, help string, opts ...ArgOption) { 258 | a.register(name, help, "int64", false, 259 | func(args []string, res ArgMap) ([]string, error) { 260 | i, err := strconv.ParseInt(args[0], 10, 64) 261 | if err != nil { 262 | return nil, fmt.Errorf("invalid int64 value '%s' for argument: %s", args[0], name) 263 | } 264 | 265 | res[name] = &ArgMapItem{Value: i} 266 | return args[1:], nil 267 | }, 268 | opts..., 269 | ) 270 | } 271 | 272 | // Int64List registers an int64 list argument. 273 | func (a *Args) Int64List(name, help string, opts ...ArgOption) { 274 | a.register(name, help, "int64 list", true, 275 | func(args []string, res ArgMap) ([]string, error) { 276 | var ( 277 | err error 278 | is = make([]int64, len(args)) 279 | ) 280 | for i, a := range args { 281 | is[i], err = strconv.ParseInt(a, 10, 64) 282 | if err != nil { 283 | return nil, fmt.Errorf("invalid int64 value '%s' for argument: %s", a, name) 284 | } 285 | } 286 | 287 | res[name] = &ArgMapItem{Value: is} 288 | return []string{}, nil 289 | }, 290 | opts..., 291 | ) 292 | } 293 | 294 | // Uint registers an uint argument. 295 | func (a *Args) Uint(name, help string, opts ...ArgOption) { 296 | a.register(name, help, "uint", false, 297 | func(args []string, res ArgMap) ([]string, error) { 298 | u, err := strconv.ParseUint(args[0], 10, 64) 299 | if err != nil { 300 | return nil, fmt.Errorf("invalid uint value '%s' for argument: %s", args[0], name) 301 | } 302 | 303 | res[name] = &ArgMapItem{Value: uint(u)} 304 | return args[1:], nil 305 | }, 306 | opts..., 307 | ) 308 | } 309 | 310 | // UintList registers an uint list argument. 311 | func (a *Args) UintList(name, help string, opts ...ArgOption) { 312 | a.register(name, help, "uint list", true, 313 | func(args []string, res ArgMap) ([]string, error) { 314 | var ( 315 | err error 316 | u uint64 317 | is = make([]uint, len(args)) 318 | ) 319 | for i, a := range args { 320 | u, err = strconv.ParseUint(a, 10, 64) 321 | if err != nil { 322 | return nil, fmt.Errorf("invalid uint value '%s' for argument: %s", a, name) 323 | } 324 | is[i] = uint(u) 325 | } 326 | 327 | res[name] = &ArgMapItem{Value: is} 328 | return []string{}, nil 329 | }, 330 | opts..., 331 | ) 332 | } 333 | 334 | // Uint64 registers an uint64 argument. 335 | func (a *Args) Uint64(name, help string, opts ...ArgOption) { 336 | a.register(name, help, "uint64", false, 337 | func(args []string, res ArgMap) ([]string, error) { 338 | u, err := strconv.ParseUint(args[0], 10, 64) 339 | if err != nil { 340 | return nil, fmt.Errorf("invalid uint64 value '%s' for argument: %s", args[0], name) 341 | } 342 | 343 | res[name] = &ArgMapItem{Value: u} 344 | return args[1:], nil 345 | }, 346 | opts..., 347 | ) 348 | } 349 | 350 | // Uint64List registers an uint64 list argument. 351 | func (a *Args) Uint64List(name, help string, opts ...ArgOption) { 352 | a.register(name, help, "uint64 list", true, 353 | func(args []string, res ArgMap) ([]string, error) { 354 | var ( 355 | err error 356 | us = make([]uint64, len(args)) 357 | ) 358 | for i, a := range args { 359 | us[i], err = strconv.ParseUint(a, 10, 64) 360 | if err != nil { 361 | return nil, fmt.Errorf("invalid uint64 value '%s' for argument: %s", a, name) 362 | } 363 | } 364 | 365 | res[name] = &ArgMapItem{Value: us} 366 | return []string{}, nil 367 | }, 368 | opts..., 369 | ) 370 | } 371 | 372 | // Float64 registers a float64 argument. 373 | func (a *Args) Float64(name, help string, opts ...ArgOption) { 374 | a.register(name, help, "float64", false, 375 | func(args []string, res ArgMap) ([]string, error) { 376 | f, err := strconv.ParseFloat(args[0], 64) 377 | if err != nil { 378 | return nil, fmt.Errorf("invalid float64 value '%s' for argument: %s", args[0], name) 379 | } 380 | 381 | res[name] = &ArgMapItem{Value: f} 382 | return args[1:], nil 383 | }, 384 | opts..., 385 | ) 386 | } 387 | 388 | // Float64List registers an float64 list argument. 389 | func (a *Args) Float64List(name, help string, opts ...ArgOption) { 390 | a.register(name, help, "float64 list", true, 391 | func(args []string, res ArgMap) ([]string, error) { 392 | var ( 393 | err error 394 | fs = make([]float64, len(args)) 395 | ) 396 | for i, a := range args { 397 | fs[i], err = strconv.ParseFloat(a, 64) 398 | if err != nil { 399 | return nil, fmt.Errorf("invalid float64 value '%s' for argument: %s", a, name) 400 | } 401 | } 402 | 403 | res[name] = &ArgMapItem{Value: fs} 404 | return []string{}, nil 405 | }, 406 | opts..., 407 | ) 408 | } 409 | 410 | // Duration registers a duration argument. 411 | func (a *Args) Duration(name, help string, opts ...ArgOption) { 412 | a.register(name, help, "duration", false, 413 | func(args []string, res ArgMap) ([]string, error) { 414 | d, err := time.ParseDuration(args[0]) 415 | if err != nil { 416 | return nil, fmt.Errorf("invalid duration value '%s' for argument: %s", args[0], name) 417 | } 418 | 419 | res[name] = &ArgMapItem{Value: d} 420 | return args[1:], nil 421 | }, 422 | opts..., 423 | ) 424 | } 425 | 426 | // DurationList registers an duration list argument. 427 | func (a *Args) DurationList(name, help string, opts ...ArgOption) { 428 | a.register(name, help, "duration list", true, 429 | func(args []string, res ArgMap) ([]string, error) { 430 | var ( 431 | err error 432 | ds = make([]time.Duration, len(args)) 433 | ) 434 | for i, a := range args { 435 | ds[i], err = time.ParseDuration(a) 436 | if err != nil { 437 | return nil, fmt.Errorf("invalid duration value '%s' for argument: %s", a, name) 438 | } 439 | } 440 | 441 | res[name] = &ArgMapItem{Value: ds} 442 | return []string{}, nil 443 | }, 444 | opts..., 445 | ) 446 | } 447 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "fmt" 29 | ) 30 | 31 | // Command is just that, a command for your application. 32 | type Command struct { 33 | // Command name. 34 | // This field is required. 35 | Name string 36 | 37 | // Command name aliases. 38 | Aliases []string 39 | 40 | // One liner help message for the command. 41 | // This field is required. 42 | Help string 43 | 44 | // More descriptive help message for the command. 45 | LongHelp string 46 | 47 | // HelpGroup defines the help group headline. 48 | // Note: this is only used for primary top-level commands. 49 | HelpGroup string 50 | 51 | // Usage should define how to use the command. 52 | // Sample: start [OPTIONS] CONTAINER [CONTAINER...] 53 | Usage string 54 | 55 | // Define all command flags within this function. 56 | Flags func(f *Flags) 57 | 58 | // Define all command arguments within this function. 59 | Args func(a *Args) 60 | 61 | // Function to execute for the command. 62 | Run func(c *Context) error 63 | 64 | // Completer is custom autocompleter for command. 65 | // It takes in command arguments and returns autocomplete options. 66 | // By default all commands get autocomplete of subcommands. 67 | // A non-nil Completer overrides the default behaviour. 68 | Completer func(prefix string, args []string) []string 69 | 70 | parent *Command 71 | flags Flags 72 | args Args 73 | commands Commands 74 | isBuiltin bool // Whenever this is a build-in command not added by the user. 75 | } 76 | 77 | func (c *Command) validate() error { 78 | if len(c.Name) == 0 { 79 | return fmt.Errorf("empty command name") 80 | } else if c.Name[0] == '-' { 81 | return fmt.Errorf("command name must not start with a '-'") 82 | } else if len(c.Help) == 0 { 83 | return fmt.Errorf("empty command help") 84 | } 85 | return nil 86 | } 87 | 88 | func (c *Command) registerFlagsAndArgs(addHelpFlag bool) { 89 | if addHelpFlag { 90 | // Add default help command. 91 | c.flags.Bool("h", "help", false, "display help") 92 | } 93 | 94 | if c.Flags != nil { 95 | c.Flags(&c.flags) 96 | } 97 | if c.Args != nil { 98 | c.Args(&c.args) 99 | } 100 | } 101 | 102 | // Parent returns the parent command or nil. 103 | func (c *Command) Parent() *Command { 104 | return c.parent 105 | } 106 | 107 | // AddCommand adds a new command. 108 | // Panics on error. 109 | func (c *Command) AddCommand(cmd *Command) { 110 | err := cmd.validate() 111 | if err != nil { 112 | panic(err) 113 | } 114 | 115 | cmd.parent = c 116 | cmd.registerFlagsAndArgs(true) 117 | 118 | c.commands.Add(cmd) 119 | } 120 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "sort" 29 | ) 30 | 31 | // Commands collection. 32 | type Commands struct { 33 | list []*Command 34 | changed bool // Used to resort if something changes. 35 | } 36 | 37 | // Add the command to the slice. 38 | // Duplicates are ignored. 39 | func (c *Commands) Add(cmd *Command) { 40 | c.list = append(c.list, cmd) 41 | c.changed = true 42 | } 43 | 44 | // Remove a command from the slice. 45 | func (c *Commands) Remove(name string) (found bool) { 46 | for index, cmd := range c.list { 47 | if cmd.Name == name { 48 | found = true 49 | c.changed = true 50 | c.list = append(c.list[:index], c.list[index+1:]...) 51 | return 52 | } 53 | } 54 | return 55 | } 56 | 57 | func (c *Commands) RemoveAll() { 58 | var builtins []*Command 59 | 60 | // Hint: There are no built-in sub commands. Ignore them. 61 | for _, cmd := range c.list { 62 | if cmd.isBuiltin { 63 | builtins = append(builtins, cmd) 64 | } 65 | } 66 | 67 | // Only keep the builtins. 68 | c.list = builtins 69 | c.changed = true 70 | } 71 | 72 | // All returns a slice of all commands. 73 | func (c *Commands) All() []*Command { 74 | return c.list 75 | } 76 | 77 | // Get the command by the name. Aliases are also checked. 78 | // Returns nil if not found. 79 | func (c *Commands) Get(name string) *Command { 80 | for _, cmd := range c.list { 81 | if cmd.Name == name { 82 | return cmd 83 | } 84 | for _, a := range cmd.Aliases { 85 | if a == name { 86 | return cmd 87 | } 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | // FindCommand searches for the final command through all children. 94 | // Returns a slice of non processed following command args. 95 | // Returns cmd=nil if not found. 96 | func (c *Commands) FindCommand(args []string) (cmd *Command, rest []string, err error) { 97 | var cmds []*Command 98 | cmds, _, rest, err = c.parse(args, nil, true) 99 | if err != nil { 100 | return 101 | } 102 | 103 | if len(cmds) > 0 { 104 | cmd = cmds[len(cmds)-1] 105 | } 106 | 107 | return 108 | } 109 | 110 | // Sort the commands by their name. 111 | func (c *Commands) Sort() { 112 | sort.Slice(c.list, func(i, j int) bool { 113 | return c.list[i].Name < c.list[j].Name 114 | }) 115 | } 116 | 117 | // SortRecursive sorts the commands by their name including all sub commands. 118 | func (c *Commands) SortRecursive() { 119 | c.Sort() 120 | for _, cmd := range c.list { 121 | cmd.commands.SortRecursive() 122 | } 123 | } 124 | 125 | func (c *Commands) hasChanged() bool { 126 | if c.changed { 127 | return true 128 | } 129 | for _, sc := range c.list { 130 | if sc.commands.hasChanged() { 131 | return true 132 | } 133 | } 134 | return false 135 | } 136 | 137 | func (c *Commands) unsetChanged() { 138 | c.changed = false 139 | for _, sc := range c.list { 140 | sc.commands.unsetChanged() 141 | } 142 | } 143 | 144 | // parse the args and return a command path to the root. 145 | // cmds slice is empty, if no command was found. 146 | func (c *Commands) parse( 147 | args []string, 148 | parentFlagMap FlagMap, 149 | skipFlagMaps bool, 150 | ) ( 151 | cmds []*Command, 152 | flagsMap FlagMap, 153 | rest []string, 154 | err error, 155 | ) { 156 | var fgs []FlagMap 157 | cur := c 158 | 159 | for len(args) > 0 && cur != nil { 160 | // Extract the command name from the arguments. 161 | name := args[0] 162 | 163 | // Try to find the command. 164 | cmd := cur.Get(name) 165 | if cmd == nil { 166 | break 167 | } 168 | 169 | args = args[1:] 170 | cmds = append(cmds, cmd) 171 | cur = &cmd.commands 172 | 173 | // Parse the command flags. 174 | fg := make(FlagMap) 175 | args, err = cmd.flags.parse(args, fg) 176 | if err != nil { 177 | return 178 | } 179 | 180 | if !skipFlagMaps { 181 | fgs = append(fgs, fg) 182 | } 183 | } 184 | 185 | if !skipFlagMaps { 186 | // Merge all the flag maps without default values. 187 | flagsMap = make(FlagMap) 188 | for i := len(fgs) - 1; i >= 0; i-- { 189 | flagsMap.copyMissingValues(fgs[i], false) 190 | } 191 | flagsMap.copyMissingValues(parentFlagMap, false) 192 | 193 | // Now include default values. This will ensure, that default values have 194 | // lower rank. 195 | for i := len(fgs) - 1; i >= 0; i-- { 196 | flagsMap.copyMissingValues(fgs[i], true) 197 | } 198 | flagsMap.copyMissingValues(parentFlagMap, true) 199 | } 200 | 201 | rest = args 202 | return 203 | } 204 | -------------------------------------------------------------------------------- /completer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "strings" 29 | 30 | shlex "github.com/desertbit/go-shlex" 31 | ) 32 | 33 | type completer struct { 34 | commands *Commands 35 | } 36 | 37 | func newCompleter(commands *Commands) *completer { 38 | return &completer{ 39 | commands: commands, 40 | } 41 | } 42 | 43 | func (c *completer) Do(line []rune, pos int) (newLine [][]rune, length int) { 44 | // Discard anything after the cursor position. 45 | // This is similar behaviour to shell/bash. 46 | line = line[:pos] 47 | 48 | var words []string 49 | if w, err := shlex.Split(string(line), true); err == nil { 50 | words = w 51 | } else { 52 | words = strings.Fields(string(line)) // fallback 53 | } 54 | 55 | prefix := "" 56 | if len(words) > 0 && pos >= 1 && line[pos-1] != ' ' { 57 | prefix = words[len(words)-1] 58 | words = words[:len(words)-1] 59 | } 60 | 61 | // Simple hack to allow auto completion for help. 62 | if len(words) > 0 && words[0] == "help" { 63 | words = words[1:] 64 | } 65 | 66 | var ( 67 | cmds *Commands 68 | flags *Flags 69 | suggestions [][]rune 70 | ) 71 | 72 | // Find the last commands list. 73 | if len(words) == 0 { 74 | cmds = c.commands 75 | } else { 76 | cmd, rest, err := c.commands.FindCommand(words) 77 | if err != nil || cmd == nil { 78 | return 79 | } 80 | 81 | // Call the custom completer if present. 82 | if cmd.Completer != nil { 83 | words = cmd.Completer(prefix, rest) 84 | for _, w := range words { 85 | suggestions = append(suggestions, []rune(strings.TrimPrefix(w, prefix))) 86 | } 87 | return suggestions, len(prefix) 88 | } 89 | 90 | // No rest must be there. 91 | if len(rest) != 0 { 92 | return 93 | } 94 | 95 | cmds = &cmd.commands 96 | flags = &cmd.flags 97 | } 98 | 99 | if len(prefix) > 0 { 100 | for _, cmd := range cmds.list { 101 | if strings.HasPrefix(cmd.Name, prefix) { 102 | suggestions = append(suggestions, []rune(strings.TrimPrefix(cmd.Name, prefix))) 103 | } 104 | for _, a := range cmd.Aliases { 105 | if strings.HasPrefix(a, prefix) { 106 | suggestions = append(suggestions, []rune(strings.TrimPrefix(a, prefix))) 107 | } 108 | } 109 | } 110 | 111 | if flags != nil { 112 | for _, f := range flags.list { 113 | if len(f.Short) > 0 { 114 | short := "-" + f.Short 115 | if len(prefix) < len(short) && strings.HasPrefix(short, prefix) { 116 | suggestions = append(suggestions, []rune(strings.TrimPrefix(short, prefix))) 117 | } 118 | } 119 | long := "--" + f.Long 120 | if len(prefix) < len(long) && strings.HasPrefix(long, prefix) { 121 | suggestions = append(suggestions, []rune(strings.TrimPrefix(long, prefix))) 122 | } 123 | } 124 | } 125 | } else { 126 | for _, cmd := range cmds.list { 127 | suggestions = append(suggestions, []rune(cmd.Name)) 128 | } 129 | if flags != nil { 130 | for _, f := range flags.list { 131 | suggestions = append(suggestions, []rune("--"+f.Long)) 132 | if len(f.Short) > 0 { 133 | suggestions = append(suggestions, []rune("-"+f.Short)) 134 | } 135 | } 136 | } 137 | } 138 | 139 | // Append an empty space to each suggestions. 140 | for i, s := range suggestions { 141 | suggestions[i] = append(s, ' ') 142 | } 143 | 144 | return suggestions, len(prefix) 145 | } 146 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "fmt" 29 | 30 | "github.com/fatih/color" 31 | ) 32 | 33 | const ( 34 | defaultMultiPrompt = "... " 35 | ) 36 | 37 | // Config specifies the application options. 38 | type Config struct { 39 | // Name specifies the application name. This field is required. 40 | Name string 41 | 42 | // Description specifies the application description. 43 | Description string 44 | 45 | // Define all app command flags within this function. 46 | Flags func(f *Flags) 47 | 48 | // Persist readline historys to file if specified. 49 | HistoryFile string 50 | 51 | // Specify the max length of historys, it's 500 by default, set it to -1 to disable history. 52 | HistoryLimit int 53 | 54 | // NoColor defines if color output should be disabled. 55 | NoColor bool 56 | 57 | // VimMode defines if Readline is to use VimMode for line navigation. 58 | VimMode bool 59 | 60 | // Prompt defines the shell prompt. 61 | Prompt string 62 | PromptColor *color.Color 63 | 64 | // MultiPrompt defines the prompt shown on multi readline. 65 | MultiPrompt string 66 | MultiPromptColor *color.Color 67 | 68 | // Some more optional color settings. 69 | ASCIILogoColor *color.Color 70 | ErrorColor *color.Color 71 | 72 | // Help styling. 73 | HelpHeadlineUnderline bool 74 | HelpSubCommands bool 75 | HelpHeadlineColor *color.Color 76 | 77 | // Override default iterrupt handler 78 | InterruptHandler func(a *App, count int) 79 | } 80 | 81 | // SetDefaults sets the default values if not set. 82 | func (c *Config) SetDefaults() { 83 | if c.HistoryLimit == 0 { 84 | c.HistoryLimit = 500 85 | } 86 | if c.PromptColor == nil { 87 | c.PromptColor = color.New(color.FgYellow, color.Bold) 88 | } 89 | if len(c.Prompt) == 0 { 90 | c.Prompt = c.Name + " » " 91 | } 92 | if c.MultiPromptColor == nil { 93 | c.MultiPromptColor = c.PromptColor 94 | } 95 | if len(c.MultiPrompt) == 0 { 96 | c.MultiPrompt = defaultMultiPrompt 97 | } 98 | if c.ASCIILogoColor == nil { 99 | c.ASCIILogoColor = c.PromptColor 100 | } 101 | if c.ErrorColor == nil { 102 | c.ErrorColor = color.New(color.FgRed, color.Bold) 103 | } 104 | } 105 | 106 | // Validate the required config fields. 107 | func (c *Config) Validate() error { 108 | if len(c.Name) == 0 { 109 | return fmt.Errorf("application name is not set") 110 | } 111 | return nil 112 | } 113 | 114 | func (c *Config) prompt() string { 115 | if c.NoColor { 116 | return c.Prompt 117 | } 118 | return c.PromptColor.Sprint(c.Prompt) 119 | } 120 | 121 | func (c *Config) multiPrompt() string { 122 | if c.NoColor { 123 | return c.MultiPrompt 124 | } 125 | return c.MultiPromptColor.Sprint(c.MultiPrompt) 126 | } 127 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | // Context defines a command context. 28 | type Context struct { 29 | // Reference to the app. 30 | App *App 31 | 32 | // Flags contains all command line flags. 33 | Flags FlagMap 34 | 35 | // Args contains all command line arguments. 36 | Args ArgMap 37 | 38 | // Cmd is the currently executing command. 39 | Command *Command 40 | } 41 | 42 | func newContext(a *App, cmd *Command, flags FlagMap, args ArgMap) *Context { 43 | return &Context{ 44 | App: a, 45 | Command: cmd, 46 | Flags: flags, 47 | Args: args, 48 | } 49 | } 50 | 51 | // Stop signalizes the app to exit. 52 | func (c *Context) Stop() { 53 | _ = c.App.Close() 54 | } 55 | -------------------------------------------------------------------------------- /flagmap.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "fmt" 29 | "time" 30 | ) 31 | 32 | // FlagMapItem holds the specific flag data. 33 | type FlagMapItem struct { 34 | Value interface{} 35 | IsDefault bool 36 | } 37 | 38 | // FlagMap holds all the parsed flag values. 39 | type FlagMap map[string]*FlagMapItem 40 | 41 | // copyMissingValues adds all missing values to the flags map. 42 | func (f FlagMap) copyMissingValues(m FlagMap, copyDefault bool) { 43 | for k, v := range m { 44 | if _, ok := f[k]; !ok { 45 | if !copyDefault && v.IsDefault { 46 | continue 47 | } 48 | f[k] = v 49 | } 50 | } 51 | } 52 | 53 | // String returns the given flag value as string. 54 | // Panics if not present. Flags must be registered. 55 | func (f FlagMap) String(long string) string { 56 | i := f[long] 57 | if i == nil { 58 | panic(fmt.Errorf("missing flag value: flag '%s' not registered", long)) 59 | } 60 | s, ok := i.Value.(string) 61 | if !ok { 62 | panic(fmt.Errorf("failed to assert flag '%s' to string", long)) 63 | } 64 | return s 65 | } 66 | 67 | // Bool returns the given flag value as boolean. 68 | // Panics if not present. Flags must be registered. 69 | func (f FlagMap) Bool(long string) bool { 70 | i := f[long] 71 | if i == nil { 72 | panic(fmt.Errorf("missing flag value: flag '%s' not registered", long)) 73 | } 74 | b, ok := i.Value.(bool) 75 | if !ok { 76 | panic(fmt.Errorf("failed to assert flag '%s' to bool", long)) 77 | } 78 | return b 79 | } 80 | 81 | // Int returns the given flag value as int. 82 | // Panics if not present. Flags must be registered. 83 | func (f FlagMap) Int(long string) int { 84 | i := f[long] 85 | if i == nil { 86 | panic(fmt.Errorf("missing flag value: flag '%s' not registered", long)) 87 | } 88 | v, ok := i.Value.(int) 89 | if !ok { 90 | panic(fmt.Errorf("failed to assert flag '%s' to int", long)) 91 | } 92 | return v 93 | } 94 | 95 | // Int64 returns the given flag value as int64. 96 | // Panics if not present. Flags must be registered. 97 | func (f FlagMap) Int64(long string) int64 { 98 | i := f[long] 99 | if i == nil { 100 | panic(fmt.Errorf("missing flag value: flag '%s' not registered", long)) 101 | } 102 | v, ok := i.Value.(int64) 103 | if !ok { 104 | panic(fmt.Errorf("failed to assert flag '%s' to int64", long)) 105 | } 106 | return v 107 | } 108 | 109 | // Uint returns the given flag value as uint. 110 | // Panics if not present. Flags must be registered. 111 | func (f FlagMap) Uint(long string) uint { 112 | i := f[long] 113 | if i == nil { 114 | panic(fmt.Errorf("missing flag value: flag '%s' not registered", long)) 115 | } 116 | v, ok := i.Value.(uint) 117 | if !ok { 118 | panic(fmt.Errorf("failed to assert flag '%s' to uint", long)) 119 | } 120 | return v 121 | } 122 | 123 | // Uint64 returns the given flag value as uint64. 124 | // Panics if not present. Flags must be registered. 125 | func (f FlagMap) Uint64(long string) uint64 { 126 | i := f[long] 127 | if i == nil { 128 | panic(fmt.Errorf("missing flag value: flag '%s' not registered", long)) 129 | } 130 | v, ok := i.Value.(uint64) 131 | if !ok { 132 | panic(fmt.Errorf("failed to assert flag '%s' to uint64", long)) 133 | } 134 | return v 135 | } 136 | 137 | // Float64 returns the given flag value as float64. 138 | // Panics if not present. Flags must be registered. 139 | func (f FlagMap) Float64(long string) float64 { 140 | i := f[long] 141 | if i == nil { 142 | panic(fmt.Errorf("missing flag value: flag '%s' not registered", long)) 143 | } 144 | v, ok := i.Value.(float64) 145 | if !ok { 146 | panic(fmt.Errorf("failed to assert flag '%s' to float64", long)) 147 | } 148 | return v 149 | } 150 | 151 | // Duration returns the given flag value as duration. 152 | // Panics if not present. Flags must be registered. 153 | func (f FlagMap) Duration(long string) time.Duration { 154 | i := f[long] 155 | if i == nil { 156 | panic(fmt.Errorf("missing flag value: flag '%s' not registered", long)) 157 | } 158 | v, ok := i.Value.(time.Duration) 159 | if !ok { 160 | panic(fmt.Errorf("failed to assert flag '%s' to duration", long)) 161 | } 162 | return v 163 | } 164 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "fmt" 29 | "sort" 30 | "strconv" 31 | "strings" 32 | "time" 33 | ) 34 | 35 | type parseFlagFunc func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) 36 | type defaultFlagFunc func(res FlagMap) 37 | 38 | type flagItem struct { 39 | Short string 40 | Long string 41 | Help string 42 | HelpArgs string 43 | HelpShowDefault bool 44 | Default interface{} 45 | } 46 | 47 | // Flags holds all the registered flags. 48 | type Flags struct { 49 | parsers []parseFlagFunc 50 | defaults map[string]defaultFlagFunc 51 | list []*flagItem 52 | } 53 | 54 | // empty returns true, if the flags are empty. 55 | func (f *Flags) empty() bool { 56 | return len(f.list) == 0 57 | } 58 | 59 | // sort the flags by their name. 60 | func (f *Flags) sort() { 61 | sort.Slice(f.list, func(i, j int) bool { 62 | return f.list[i].Long < f.list[j].Long 63 | }) 64 | } 65 | 66 | func (f *Flags) register( 67 | short, long, help, helpArgs string, 68 | helpShowDefault bool, 69 | defaultValue interface{}, 70 | df defaultFlagFunc, 71 | pf parseFlagFunc, 72 | ) { 73 | // Validate. 74 | if len(short) > 1 { 75 | panic(fmt.Errorf("invalid short flag: '%s': must be a single character", short)) 76 | } else if strings.HasPrefix(short, "-") { 77 | panic(fmt.Errorf("invalid short flag: '%s': must not start with a '-'", short)) 78 | } else if len(long) == 0 { 79 | panic(fmt.Errorf("empty long flag: short='%s'", short)) 80 | } else if strings.HasPrefix(long, "-") { 81 | panic(fmt.Errorf("invalid long flag: '%s': must not start with a '-'", long)) 82 | } else if len(help) == 0 { 83 | panic(fmt.Errorf("empty flag help message for flag: '%s'", long)) 84 | } 85 | 86 | // Check, that both short and long are unique. 87 | // Short flags are empty if not set. 88 | for _, fi := range f.list { 89 | if fi.Short != "" && short != "" && fi.Short == short { 90 | panic(fmt.Errorf("flag shortcut '%s' registered twice", short)) 91 | } 92 | if fi.Long == long { 93 | panic(fmt.Errorf("flag '%s' registered twice", long)) 94 | } 95 | } 96 | 97 | f.list = append(f.list, &flagItem{ 98 | Short: short, 99 | Long: long, 100 | Help: help, 101 | HelpShowDefault: helpShowDefault, 102 | HelpArgs: helpArgs, 103 | Default: defaultValue, 104 | }) 105 | 106 | if f.defaults == nil { 107 | f.defaults = make(map[string]defaultFlagFunc) 108 | } 109 | f.defaults[long] = df 110 | 111 | f.parsers = append(f.parsers, pf) 112 | } 113 | 114 | func (f *Flags) match(flag, short, long string) bool { 115 | return (len(short) > 0 && flag == "-"+short) || 116 | (len(long) > 0 && flag == "--"+long) 117 | } 118 | 119 | func (f *Flags) parse(args []string, res FlagMap) ([]string, error) { 120 | var err error 121 | var parsed bool 122 | 123 | // Parse all leading flags. 124 | Loop: 125 | for len(args) > 0 { 126 | a := args[0] 127 | if !strings.HasPrefix(a, "-") { 128 | break Loop 129 | } 130 | args = args[1:] 131 | 132 | // A double dash (--) is used to signify the end of command options, 133 | // after which only positional arguments are accepted. 134 | if a == "--" { 135 | break Loop 136 | } 137 | 138 | pos := strings.Index(a, "=") 139 | equalVal := "" 140 | if pos > 0 { 141 | equalVal = a[pos+1:] 142 | a = a[:pos] 143 | } 144 | 145 | for _, p := range f.parsers { 146 | args, parsed, err = p(a, equalVal, args, res) 147 | if err != nil { 148 | return nil, err 149 | } else if parsed { 150 | continue Loop 151 | } 152 | } 153 | return nil, fmt.Errorf("invalid flag: %s", a) 154 | } 155 | 156 | // Finally set all the default values for not passed flags. 157 | if f.defaults == nil { 158 | return args, nil 159 | } 160 | 161 | for _, i := range f.list { 162 | if _, ok := res[i.Long]; ok { 163 | continue 164 | } 165 | df, ok := f.defaults[i.Long] 166 | if !ok { 167 | return nil, fmt.Errorf("invalid flag: missing default function: %s", i.Long) 168 | } 169 | df(res) 170 | } 171 | 172 | return args, nil 173 | } 174 | 175 | // StringL same as String, but without a shorthand. 176 | func (f *Flags) StringL(long, defaultValue, help string) { 177 | f.String("", long, defaultValue, help) 178 | } 179 | 180 | // String registers a string flag. 181 | func (f *Flags) String(short, long, defaultValue, help string) { 182 | f.register(short, long, help, "string", true, defaultValue, 183 | func(res FlagMap) { 184 | res[long] = &FlagMapItem{ 185 | Value: defaultValue, 186 | IsDefault: true, 187 | } 188 | }, 189 | func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) { 190 | if !f.match(flag, short, long) { 191 | return args, false, nil 192 | } 193 | if len(equalVal) > 0 { 194 | res[long] = &FlagMapItem{ 195 | Value: trimQuotes(equalVal), 196 | IsDefault: false, 197 | } 198 | return args, true, nil 199 | } 200 | if len(args) == 0 { 201 | return args, false, fmt.Errorf("missing string value for flag: %s", flag) 202 | } 203 | res[long] = &FlagMapItem{ 204 | Value: args[0], 205 | IsDefault: false, 206 | } 207 | args = args[1:] 208 | return args, true, nil 209 | }) 210 | } 211 | 212 | // BoolL same as Bool, but without a shorthand. 213 | func (f *Flags) BoolL(long string, defaultValue bool, help string) { 214 | f.Bool("", long, defaultValue, help) 215 | } 216 | 217 | // Bool registers a boolean flag. 218 | func (f *Flags) Bool(short, long string, defaultValue bool, help string) { 219 | f.register(short, long, help, "", false, defaultValue, 220 | func(res FlagMap) { 221 | res[long] = &FlagMapItem{ 222 | Value: defaultValue, 223 | IsDefault: true, 224 | } 225 | }, 226 | func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) { 227 | if !f.match(flag, short, long) { 228 | return args, false, nil 229 | } 230 | if len(equalVal) > 0 { 231 | b, err := strconv.ParseBool(equalVal) 232 | if err != nil { 233 | return args, false, fmt.Errorf("invalid boolean value for flag: %s", flag) 234 | } 235 | res[long] = &FlagMapItem{ 236 | Value: b, 237 | IsDefault: false, 238 | } 239 | return args, true, nil 240 | } 241 | res[long] = &FlagMapItem{ 242 | Value: true, 243 | IsDefault: false, 244 | } 245 | return args, true, nil 246 | }) 247 | } 248 | 249 | // IntL same as Int, but without a shorthand. 250 | func (f *Flags) IntL(long string, defaultValue int, help string) { 251 | f.Int("", long, defaultValue, help) 252 | } 253 | 254 | // Int registers an int flag. 255 | func (f *Flags) Int(short, long string, defaultValue int, help string) { 256 | f.register(short, long, help, "int", true, defaultValue, 257 | func(res FlagMap) { 258 | res[long] = &FlagMapItem{ 259 | Value: defaultValue, 260 | IsDefault: true, 261 | } 262 | }, 263 | func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) { 264 | if !f.match(flag, short, long) { 265 | return args, false, nil 266 | } 267 | var vStr string 268 | if len(equalVal) > 0 { 269 | vStr = equalVal 270 | } else if len(args) > 0 { 271 | vStr = args[0] 272 | args = args[1:] 273 | } else { 274 | return args, false, fmt.Errorf("missing int value for flag: %s", flag) 275 | } 276 | i, err := strconv.Atoi(vStr) 277 | if err != nil { 278 | return args, false, fmt.Errorf("invalid int value for flag: %s", flag) 279 | } 280 | res[long] = &FlagMapItem{ 281 | Value: i, 282 | IsDefault: false, 283 | } 284 | return args, true, nil 285 | }) 286 | } 287 | 288 | // Int64L same as Int64, but without a shorthand. 289 | func (f *Flags) Int64L(long string, defaultValue int64, help string) { 290 | f.Int64("", long, defaultValue, help) 291 | } 292 | 293 | // Int64 registers an int64 flag. 294 | func (f *Flags) Int64(short, long string, defaultValue int64, help string) { 295 | f.register(short, long, help, "int", true, defaultValue, 296 | func(res FlagMap) { 297 | res[long] = &FlagMapItem{ 298 | Value: defaultValue, 299 | IsDefault: true, 300 | } 301 | }, 302 | func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) { 303 | if !f.match(flag, short, long) { 304 | return args, false, nil 305 | } 306 | var vStr string 307 | if len(equalVal) > 0 { 308 | vStr = equalVal 309 | } else if len(args) > 0 { 310 | vStr = args[0] 311 | args = args[1:] 312 | } else { 313 | return args, false, fmt.Errorf("missing int value for flag: %s", flag) 314 | } 315 | i, err := strconv.ParseInt(vStr, 10, 64) 316 | if err != nil { 317 | return args, false, fmt.Errorf("invalid int value for flag: %s", flag) 318 | } 319 | res[long] = &FlagMapItem{ 320 | Value: i, 321 | IsDefault: false, 322 | } 323 | return args, true, nil 324 | }) 325 | } 326 | 327 | // UintL same as Uint, but without a shorthand. 328 | func (f *Flags) UintL(long string, defaultValue uint, help string) { 329 | f.Uint("", long, defaultValue, help) 330 | } 331 | 332 | // Uint registers an uint flag. 333 | func (f *Flags) Uint(short, long string, defaultValue uint, help string) { 334 | f.register(short, long, help, "uint", true, defaultValue, 335 | func(res FlagMap) { 336 | res[long] = &FlagMapItem{ 337 | Value: defaultValue, 338 | IsDefault: true, 339 | } 340 | }, 341 | func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) { 342 | if !f.match(flag, short, long) { 343 | return args, false, nil 344 | } 345 | var vStr string 346 | if len(equalVal) > 0 { 347 | vStr = equalVal 348 | } else if len(args) > 0 { 349 | vStr = args[0] 350 | args = args[1:] 351 | } else { 352 | return args, false, fmt.Errorf("missing uint value for flag: %s", flag) 353 | } 354 | i, err := strconv.ParseUint(vStr, 10, 64) 355 | if err != nil { 356 | return args, false, fmt.Errorf("invalid uint value for flag: %s", flag) 357 | } 358 | res[long] = &FlagMapItem{ 359 | Value: uint(i), 360 | IsDefault: false, 361 | } 362 | return args, true, nil 363 | }) 364 | } 365 | 366 | // Uint64L same as Uint64, but without a shorthand. 367 | func (f *Flags) Uint64L(long string, defaultValue uint64, help string) { 368 | f.Uint64("", long, defaultValue, help) 369 | } 370 | 371 | // Uint64 registers an uint64 flag. 372 | func (f *Flags) Uint64(short, long string, defaultValue uint64, help string) { 373 | f.register(short, long, help, "uint", true, defaultValue, 374 | func(res FlagMap) { 375 | res[long] = &FlagMapItem{ 376 | Value: defaultValue, 377 | IsDefault: true, 378 | } 379 | }, 380 | func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) { 381 | if !f.match(flag, short, long) { 382 | return args, false, nil 383 | } 384 | var vStr string 385 | if len(equalVal) > 0 { 386 | vStr = equalVal 387 | } else if len(args) > 0 { 388 | vStr = args[0] 389 | args = args[1:] 390 | } else { 391 | return args, false, fmt.Errorf("missing uint value for flag: %s", flag) 392 | } 393 | i, err := strconv.ParseUint(vStr, 10, 64) 394 | if err != nil { 395 | return args, false, fmt.Errorf("invalid uint value for flag: %s", flag) 396 | } 397 | res[long] = &FlagMapItem{ 398 | Value: i, 399 | IsDefault: false, 400 | } 401 | return args, true, nil 402 | }) 403 | } 404 | 405 | // Float64L same as Float64, but without a shorthand. 406 | func (f *Flags) Float64L(long string, defaultValue float64, help string) { 407 | f.Float64("", long, defaultValue, help) 408 | } 409 | 410 | // Float64 registers an float64 flag. 411 | func (f *Flags) Float64(short, long string, defaultValue float64, help string) { 412 | f.register(short, long, help, "float", true, defaultValue, 413 | func(res FlagMap) { 414 | res[long] = &FlagMapItem{ 415 | Value: defaultValue, 416 | IsDefault: true, 417 | } 418 | }, 419 | func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) { 420 | if !f.match(flag, short, long) { 421 | return args, false, nil 422 | } 423 | var vStr string 424 | if len(equalVal) > 0 { 425 | vStr = equalVal 426 | } else if len(args) > 0 { 427 | vStr = args[0] 428 | args = args[1:] 429 | } else { 430 | return args, false, fmt.Errorf("missing float value for flag: %s", flag) 431 | } 432 | i, err := strconv.ParseFloat(vStr, 64) 433 | if err != nil { 434 | return args, false, fmt.Errorf("invalid float value for flag: %s", flag) 435 | } 436 | res[long] = &FlagMapItem{ 437 | Value: i, 438 | IsDefault: false, 439 | } 440 | return args, true, nil 441 | }) 442 | } 443 | 444 | // DurationL same as Duration, but without a shorthand. 445 | func (f *Flags) DurationL(long string, defaultValue time.Duration, help string) { 446 | f.Duration("", long, defaultValue, help) 447 | } 448 | 449 | // Duration registers a duration flag. 450 | func (f *Flags) Duration(short, long string, defaultValue time.Duration, help string) { 451 | f.register(short, long, help, "duration", true, defaultValue, 452 | func(res FlagMap) { 453 | res[long] = &FlagMapItem{ 454 | Value: defaultValue, 455 | IsDefault: true, 456 | } 457 | }, 458 | func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) { 459 | if !f.match(flag, short, long) { 460 | return args, false, nil 461 | } 462 | var vStr string 463 | if len(equalVal) > 0 { 464 | vStr = equalVal 465 | } else if len(args) > 0 { 466 | vStr = args[0] 467 | args = args[1:] 468 | } else { 469 | return args, false, fmt.Errorf("missing duration value for flag: %s", flag) 470 | } 471 | d, err := time.ParseDuration(vStr) 472 | if err != nil { 473 | return args, false, fmt.Errorf("invalid duration value for flag: %s", flag) 474 | } 475 | res[long] = &FlagMapItem{ 476 | Value: d, 477 | IsDefault: false, 478 | } 479 | return args, true, nil 480 | }) 481 | } 482 | 483 | func trimQuotes(s string) string { 484 | if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' { 485 | return s[1 : len(s)-1] 486 | } 487 | return s 488 | } 489 | -------------------------------------------------------------------------------- /functions.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package grumble 26 | 27 | import ( 28 | "fmt" 29 | "os" 30 | "sort" 31 | 32 | "github.com/desertbit/columnize" 33 | ) 34 | 35 | func defaultInterruptHandler(a *App, count int) { 36 | if count >= 2 { 37 | a.Println("interrupted") 38 | os.Exit(1) 39 | } 40 | a.Println("input Ctrl-c once more to exit") 41 | } 42 | 43 | func defaultPrintHelp(a *App, shell bool) { 44 | // Columnize options. 45 | config := columnize.DefaultConfig() 46 | config.Delim = "|" 47 | config.Glue = " " 48 | config.Prefix = " " 49 | 50 | // ASCII logo. 51 | if a.printASCIILogo != nil { 52 | a.printASCIILogo(a) 53 | } 54 | 55 | // Description. 56 | if (len(a.config.Description)) > 0 { 57 | a.Printf("\n%s\n", a.config.Description) 58 | } 59 | 60 | // Usage. 61 | if !shell { 62 | a.Println() 63 | printHeadline(a, "Usage:") 64 | a.Printf(" %s [command]\n", a.config.Name) 65 | } 66 | 67 | // Group the commands by their help group if present. 68 | groups := make(map[string]*Commands) 69 | for _, c := range a.commands.list { 70 | key := c.HelpGroup 71 | if len(key) == 0 { 72 | key = "Commands:" 73 | } 74 | cc := groups[key] 75 | if cc == nil { 76 | cc = new(Commands) 77 | groups[key] = cc 78 | } 79 | cc.Add(c) 80 | } 81 | 82 | // Sort the map by the keys. 83 | var keys []string 84 | for k := range groups { 85 | keys = append(keys, k) 86 | } 87 | sort.Strings(keys) 88 | 89 | // Print each commands group. 90 | for _, headline := range keys { 91 | cc := groups[headline] 92 | cc.Sort() 93 | 94 | var output []string 95 | for _, c := range cc.list { 96 | name := c.Name 97 | for _, a := range c.Aliases { 98 | name += ", " + a 99 | } 100 | output = append(output, fmt.Sprintf("%s | %v", name, c.Help)) 101 | } 102 | 103 | if len(output) > 0 { 104 | a.Println() 105 | printHeadline(a, headline) 106 | a.Printf("%s\n", columnize.Format(output, config)) 107 | } 108 | } 109 | 110 | // Sub Commands. 111 | if a.config.HelpSubCommands { 112 | // Check if there is at least one sub command. 113 | hasSubCmds := false 114 | for _, c := range a.commands.list { 115 | if len(c.commands.list) > 0 { 116 | hasSubCmds = true 117 | break 118 | } 119 | } 120 | if hasSubCmds { 121 | // Headline. 122 | a.Println() 123 | printHeadline(a, "Sub Commands:") 124 | hp := headlinePrinter(a) 125 | 126 | // Only print the first level of sub commands. 127 | for _, c := range a.commands.list { 128 | if len(c.commands.list) == 0 { 129 | continue 130 | } 131 | 132 | var output []string 133 | for _, c := range c.commands.list { 134 | name := c.Name 135 | for _, a := range c.Aliases { 136 | name += ", " + a 137 | } 138 | output = append(output, fmt.Sprintf("%s | %v", name, c.Help)) 139 | } 140 | 141 | a.Println() 142 | _, _ = hp(c.Name + ":") 143 | a.Printf("%s\n", columnize.Format(output, config)) 144 | } 145 | } 146 | } 147 | 148 | // Flags. 149 | if !shell { 150 | printFlags(a, &a.flags) 151 | } 152 | 153 | a.Println() 154 | } 155 | 156 | func defaultPrintCommandHelp(a *App, cmd *Command, shell bool) { 157 | // Columnize options. 158 | config := columnize.DefaultConfig() 159 | config.Delim = "|" 160 | config.Glue = " " 161 | config.Prefix = " " 162 | 163 | // Help description. 164 | if len(cmd.LongHelp) > 0 { 165 | a.Printf("\n%s\n", cmd.LongHelp) 166 | } else { 167 | a.Printf("\n%s\n", cmd.Help) 168 | } 169 | 170 | // Usage. 171 | printUsage(a, cmd) 172 | 173 | // Arguments. 174 | printArgs(a, &cmd.args) 175 | 176 | // Flags. 177 | printFlags(a, &cmd.flags) 178 | 179 | // Sub Commands. 180 | if len(cmd.commands.list) > 0 { 181 | // Only print the first level of sub commands. 182 | var output []string 183 | for _, c := range cmd.commands.list { 184 | name := c.Name 185 | for _, a := range c.Aliases { 186 | name += ", " + a 187 | } 188 | output = append(output, fmt.Sprintf("%s | %v", name, c.Help)) 189 | } 190 | 191 | a.Println() 192 | printHeadline(a, "Sub Commands:") 193 | a.Printf("%s\n", columnize.Format(output, config)) 194 | } 195 | 196 | a.Println() 197 | } 198 | 199 | func headlinePrinter(a *App) func(v ...interface{}) (int, error) { 200 | if a.config.NoColor || a.config.HelpHeadlineColor == nil { 201 | return a.Println 202 | } 203 | return func(v ...interface{}) (int, error) { 204 | return a.config.HelpHeadlineColor.Fprintln(a, v...) 205 | } 206 | } 207 | 208 | func printHeadline(a *App, s string) { 209 | hp := headlinePrinter(a) 210 | if a.config.HelpHeadlineUnderline { 211 | _, _ = hp(s) 212 | u := "" 213 | for i := 0; i < len(s); i++ { 214 | u += "=" 215 | } 216 | _, _ = hp(u) 217 | } else { 218 | _, _ = hp(s) 219 | } 220 | } 221 | 222 | func printUsage(a *App, cmd *Command) { 223 | a.Println() 224 | printHeadline(a, "Usage:") 225 | 226 | // Print either the user-provided usage message or compose 227 | // one on our own from the flags and args. 228 | if len(cmd.Usage) > 0 { 229 | a.Printf(" %s\n", cmd.Usage) 230 | return 231 | } 232 | 233 | // Layout: Cmd [Flags] Args 234 | a.Printf(" %s", cmd.Name) 235 | if !cmd.flags.empty() { 236 | a.Printf(" [flags]") 237 | } 238 | if !cmd.args.empty() { 239 | for _, arg := range cmd.args.list { 240 | name := arg.Name 241 | if arg.isList { 242 | name += "..." 243 | } 244 | 245 | if arg.optional { 246 | a.Printf(" [%s]", name) 247 | } else { 248 | a.Printf(" %s", name) 249 | } 250 | 251 | if arg.isList && (arg.listMin != -1 || arg.listMax != -1) { 252 | a.Printf("{") 253 | if arg.listMin != -1 { 254 | a.Printf("%d", arg.listMin) 255 | } 256 | a.Printf(",") 257 | if arg.listMax != -1 { 258 | a.Printf("%d", arg.listMax) 259 | } 260 | a.Printf("}") 261 | } 262 | } 263 | } 264 | a.Println() 265 | } 266 | 267 | func printArgs(a *App, args *Args) { 268 | // Columnize options. 269 | config := columnize.DefaultConfig() 270 | config.Delim = "|" 271 | config.Glue = " " 272 | config.Prefix = " " 273 | 274 | var output []string 275 | for _, a := range args.list { 276 | defaultValue := "" 277 | if a.Default != nil && len(fmt.Sprintf("%v", a.Default)) > 0 && a.optional { 278 | defaultValue = fmt.Sprintf("(default: %v)", a.Default) 279 | } 280 | output = append(output, fmt.Sprintf("%s || %s |||| %s %s", a.Name, a.HelpArgs, a.Help, defaultValue)) 281 | } 282 | 283 | if len(output) > 0 { 284 | a.Println() 285 | printHeadline(a, "Args:") 286 | a.Printf("%s\n", columnize.Format(output, config)) 287 | } 288 | } 289 | 290 | func printFlags(a *App, flags *Flags) { 291 | // Columnize options. 292 | config := columnize.DefaultConfig() 293 | config.Delim = "|" 294 | config.Glue = " " 295 | config.Prefix = " " 296 | 297 | flags.sort() 298 | 299 | var output []string 300 | for _, f := range flags.list { 301 | long := "--" + f.Long 302 | short := "" 303 | if len(f.Short) > 0 { 304 | short = "-" + f.Short + "," 305 | } 306 | 307 | defaultValue := "" 308 | if f.Default != nil && f.HelpShowDefault && len(fmt.Sprintf("%v", f.Default)) > 0 { 309 | defaultValue = fmt.Sprintf("(default: %v)", f.Default) 310 | } 311 | 312 | output = append(output, fmt.Sprintf("%s | %s | %s |||| %s %s", short, long, f.HelpArgs, f.Help, defaultValue)) 313 | } 314 | 315 | if len(output) > 0 { 316 | a.Println() 317 | printHeadline(a, "Flags:") 318 | a.Printf("%s\n", columnize.Format(output, config)) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/desertbit/grumble 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667 // indirect 7 | github.com/desertbit/closer/v3 v3.1.3 8 | github.com/desertbit/columnize v2.1.0+incompatible 9 | github.com/desertbit/go-shlex v0.1.1 10 | github.com/desertbit/readline v1.5.1 11 | github.com/fatih/color v1.14.1 12 | github.com/hashicorp/errwrap v1.1.0 // indirect 13 | github.com/hashicorp/go-multierror v1.1.1 // indirect 14 | github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c // indirect 15 | github.com/kr/pty v1.1.8 // indirect 16 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect 17 | golang.org/x/crypto v0.31.0 // indirect 18 | gopkg.in/AlecAivazis/survey.v1 v1.8.8 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74= 2 | github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 3 | github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667 h1:l2RCK7mjLhjfZRIcCXTVHI34l67IRtKASBjusViLzQ0= 4 | github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= 5 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 6 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 7 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 9 | github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= 10 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/desertbit/closer/v3 v3.1.3 h1:WUxD92dKrXuqSgbiAsqN6MPdnli2xIjkn6WpyC5AzXE= 15 | github.com/desertbit/closer/v3 v3.1.3/go.mod h1:AAC4KRd8DC40nwvV967J/kDFhujMEiuwIKQfN0IDxXw= 16 | github.com/desertbit/columnize v2.1.0+incompatible h1:h55rYmdrWoTj7w9aAnCkxzM3C2Eb8zuFa2W41t0o5j0= 17 | github.com/desertbit/columnize v2.1.0+incompatible/go.mod h1:5kPrzQwKbQ8E5D28nvTVPqIBJyj+8jvJzwt6HXZvXgI= 18 | github.com/desertbit/go-shlex v0.1.1 h1:c65HnbgX1QyC6kPL1dMzUpZ4puNUE6ai/eVucWNLNsk= 19 | github.com/desertbit/go-shlex v0.1.1/go.mod h1:Qbb+mJNud5AypgHZ81EL8syOGaWlwvAOTqS7XmWI4pQ= 20 | github.com/desertbit/readline v1.5.1 h1:/wOIZkWYl1s+IvJm/5bOknfUgs6MhS9svRNZpFM53Os= 21 | github.com/desertbit/readline v1.5.1/go.mod h1:pHQgTsCFs9Cpfh5mlSUFi9Xa5kkL4d8L1Jo4UVWzPw0= 22 | github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= 23 | github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= 24 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 25 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 26 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 28 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 29 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 30 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 31 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 32 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 33 | github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 34 | github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c h1:kp3AxgXgDOmIJFR7bIwqFhwJ2qWar8tEQSE5XXhCfVk= 35 | github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= 36 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 37 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 38 | github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 39 | github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= 40 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 41 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 42 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 43 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 44 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 45 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 46 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 47 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 48 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 49 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= 50 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 51 | github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 56 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 57 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 58 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 59 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 60 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 61 | golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 62 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 63 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 64 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 65 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 66 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 67 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 68 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 69 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 70 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 71 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 72 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 73 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 74 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 75 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 76 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 77 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 78 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 79 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 80 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 81 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 82 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 83 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 84 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 85 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 86 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 87 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 88 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 89 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 90 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 91 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 96 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 98 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 99 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 104 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 105 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 106 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 107 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 108 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 109 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 110 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 111 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 112 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 113 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 114 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 115 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 116 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 117 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 118 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 119 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 120 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 121 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 122 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 123 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 124 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 125 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 126 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 127 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 128 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 129 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 130 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 131 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 132 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 133 | gopkg.in/AlecAivazis/survey.v1 v1.8.8 h1:5UtTowJZTz1j7NxVzDGKTz6Lm9IWm8DDF6b7a2wq9VY= 134 | gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 136 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 137 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 138 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 139 | -------------------------------------------------------------------------------- /grumble.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | // Package grumble is a powerful modern CLI and SHELL. 26 | package grumble 27 | 28 | import ( 29 | "fmt" 30 | "os" 31 | ) 32 | 33 | // Main is a shorthand to run the app within the main function. 34 | // This function will handle the error and exit the application on error. 35 | func Main(a *App) { 36 | err := a.Run() 37 | if err != nil { 38 | _, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err) 39 | os.Exit(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/full/cmd/admin.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "fmt" 29 | 30 | "github.com/desertbit/grumble" 31 | ) 32 | 33 | func init() { 34 | adminCommand := &grumble.Command{ 35 | Name: "admin", 36 | Help: "admin tools", 37 | LongHelp: "super administration tools", 38 | } 39 | App.AddCommand(adminCommand) 40 | 41 | adminCommand.AddCommand(&grumble.Command{ 42 | Name: "root", 43 | Help: "root the machine", 44 | Run: func(c *grumble.Context) error { 45 | fmt.Println(c.Flags.String("directory")) 46 | return fmt.Errorf("failed") 47 | }, 48 | }) 49 | 50 | adminCommand.AddCommand(&grumble.Command{ 51 | Name: "kill", 52 | Help: "kill the process", 53 | Run: func(c *grumble.Context) error { 54 | return fmt.Errorf("failed") 55 | }, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /sample/full/cmd/app.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "github.com/desertbit/grumble" 29 | "github.com/fatih/color" 30 | ) 31 | 32 | var App = grumble.New(&grumble.Config{ 33 | Name: "foo", 34 | Description: "An awesome foo bar", 35 | HistoryFile: "/tmp/foo.hist", 36 | Prompt: "foo » ", 37 | PromptColor: color.New(color.FgGreen, color.Bold), 38 | HelpHeadlineColor: color.New(color.FgGreen), 39 | HelpHeadlineUnderline: true, 40 | HelpSubCommands: true, 41 | 42 | Flags: func(f *grumble.Flags) { 43 | f.String("d", "directory", "DEFAULT", "set an alternative root directory path") 44 | f.Bool("v", "verbose", false, "enable verbose mode") 45 | }, 46 | }) 47 | 48 | func init() { 49 | App.SetPrintASCIILogo(func(a *grumble.App) { 50 | a.Println(" _ _ ") 51 | a.Println(" ___ ___ _ _ _____| |_| |___ ") 52 | a.Println("| . | _| | | | . | | -_|") 53 | a.Println("|_ |_| |___|_|_|_|___|_|___|") 54 | a.Println("|___| ") 55 | a.Println() 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /sample/full/cmd/args.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "fmt" 29 | "strings" 30 | "time" 31 | 32 | "github.com/desertbit/grumble" 33 | ) 34 | 35 | func init() { 36 | App.AddCommand(&grumble.Command{ 37 | Name: "args", 38 | Help: "test args", 39 | Args: func(a *grumble.Args) { 40 | a.String("s", "test string") 41 | a.Duration("d", "test duration", grumble.Default(time.Second)) 42 | a.Int("i", "test int", grumble.Default(5)) 43 | a.Int64("i64", "test int64", grumble.Default(int64(-88))) 44 | a.Uint("u", "test uint", grumble.Default(uint(66))) 45 | a.Uint64("u64", "test uint64", grumble.Default(uint64(8888))) 46 | a.Float64("f64", "test float64", grumble.Default(float64(5.889))) 47 | a.StringList("sl", "test string list", grumble.Default([]string{"first", "second", "third"}), grumble.Max(3)) 48 | }, 49 | Run: func(c *grumble.Context) error { 50 | fmt.Println("s ", c.Args.String("s")) 51 | fmt.Println("d ", c.Args.Duration("d")) 52 | fmt.Println("i ", c.Args.Int("i")) 53 | fmt.Println("i64", c.Args.Int64("i64")) 54 | fmt.Println("u ", c.Args.Uint("u")) 55 | fmt.Println("u64", c.Args.Uint64("u64")) 56 | fmt.Println("f64", c.Args.Float64("f64")) 57 | fmt.Println("sl ", strings.Join(c.Args.StringList("sl"), ",")) 58 | return nil 59 | }, 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /sample/full/cmd/ask.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "fmt" 29 | 30 | "github.com/desertbit/grumble" 31 | "gopkg.in/AlecAivazis/survey.v1" 32 | ) 33 | 34 | func init() { 35 | App.AddCommand(&grumble.Command{ 36 | Name: "ask", 37 | Help: "ask the user for foo", 38 | Run: func(c *grumble.Context) error { 39 | ask() 40 | return nil 41 | }, 42 | }) 43 | } 44 | 45 | // the questions to ask 46 | var qs = []*survey.Question{ 47 | { 48 | Name: "name", 49 | Prompt: &survey.Input{Message: "What is your name?"}, 50 | Validate: survey.Required, 51 | Transform: survey.Title, 52 | }, 53 | { 54 | Name: "color", 55 | Prompt: &survey.Select{ 56 | Message: "Choose a color:", 57 | Options: []string{"red", "blue", "green"}, 58 | Default: "red", 59 | }, 60 | }, 61 | { 62 | Name: "age", 63 | Prompt: &survey.Input{Message: "How old are you?"}, 64 | }, 65 | } 66 | 67 | func ask() { 68 | password := "" 69 | prompt := &survey.Password{ 70 | Message: "Please type your password", 71 | } 72 | survey.AskOne(prompt, &password, nil) 73 | 74 | // the answers will be written to this struct 75 | answers := struct { 76 | Name string // survey will match the question and field names 77 | FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name 78 | Age int // if the types don't match exactly, survey will try to convert for you 79 | }{} 80 | 81 | // perform the questions 82 | err := survey.Ask(qs, &answers) 83 | if err != nil { 84 | fmt.Println(err.Error()) 85 | return 86 | } 87 | 88 | fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor) 89 | } 90 | -------------------------------------------------------------------------------- /sample/full/cmd/daemon.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "strings" 29 | "time" 30 | 31 | "github.com/desertbit/grumble" 32 | ) 33 | 34 | func init() { 35 | App.AddCommand(&grumble.Command{ 36 | Name: "daemon", 37 | Help: "run the daemon", 38 | Aliases: []string{"run"}, 39 | Flags: func(f *grumble.Flags) { 40 | f.Duration("t", "timeout", time.Second, "timeout duration") 41 | }, 42 | Args: func(a *grumble.Args) { 43 | a.Bool("production", "whether to start the daemon in production or development mode") 44 | a.Int("opt-level", "the optimization mode", grumble.Default(3)) 45 | a.StringList("services", "additional services that should be started", grumble.Default([]string{"test", "te11"})) 46 | }, 47 | Run: func(c *grumble.Context) error { 48 | c.App.Println("timeout:", c.Flags.Duration("timeout")) 49 | c.App.Println("directory:", c.Flags.String("directory")) 50 | c.App.Println("verbose:", c.Flags.Bool("verbose")) 51 | c.App.Println("production:", c.Args.Bool("production")) 52 | c.App.Println("opt-level:", c.Args.Int("opt-level")) 53 | c.App.Println("services:", strings.Join(c.Args.StringList("services"), ",")) 54 | return nil 55 | }, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /sample/full/cmd/flags.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "fmt" 29 | "time" 30 | 31 | "github.com/desertbit/grumble" 32 | ) 33 | 34 | func init() { 35 | App.AddCommand(&grumble.Command{ 36 | Name: "flags", 37 | Help: "test flags", 38 | Flags: func(f *grumble.Flags) { 39 | f.Duration("d", "duration", time.Second, "duration test") 40 | f.Int("i", "int", 1, "test int") 41 | f.Int64("l", "int64", 2, "test int64") 42 | f.Uint("u", "uint", 3, "test uint") 43 | f.Uint64("j", "uint64", 4, "test uint64") 44 | f.Float64("f", "float", 5.55, "test float64") 45 | }, 46 | Run: func(c *grumble.Context) error { 47 | fmt.Println("duration ", c.Flags.Duration("duration")) 48 | fmt.Println("int ", c.Flags.Int("int")) 49 | fmt.Println("int64 ", c.Flags.Int64("int64")) 50 | fmt.Println("uint ", c.Flags.Uint("uint")) 51 | fmt.Println("uint64 ", c.Flags.Uint64("uint64")) 52 | fmt.Println("float ", c.Flags.Float64("float")) 53 | return nil 54 | }, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /sample/full/cmd/prompt.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "github.com/desertbit/grumble" 29 | ) 30 | 31 | func init() { 32 | promptCommand := &grumble.Command{ 33 | Name: "prompt", 34 | Help: "set a custom prompt", 35 | } 36 | App.AddCommand(promptCommand) 37 | 38 | promptCommand.AddCommand(&grumble.Command{ 39 | Name: "set", 40 | Help: "set a custom prompt", 41 | Run: func(c *grumble.Context) error { 42 | c.App.SetPrompt("CUSTOM PROMPT >> ") 43 | return nil 44 | }, 45 | }) 46 | 47 | promptCommand.AddCommand(&grumble.Command{ 48 | Name: "reset", 49 | Help: "reset to default prompt", 50 | Run: func(c *grumble.Context) error { 51 | c.App.SetDefaultPrompt() 52 | return nil 53 | }, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /sample/full/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package main 26 | 27 | import ( 28 | "github.com/desertbit/grumble" 29 | "github.com/desertbit/grumble/sample/full/cmd" 30 | ) 31 | 32 | func main() { 33 | grumble.Main(cmd.App) 34 | } 35 | -------------------------------------------------------------------------------- /sample/readline/README.md: -------------------------------------------------------------------------------- 1 | ## Remote readline example 2 | This is demonstration of remote shell access with grumble. 3 | 4 | ### How to run 5 | First run the server in main directory: 6 | 7 | ```shell 8 | ./readline> go run . 9 | ``` 10 | 11 | Then establish a CLI connection with client app in **cli** subfolder 12 | 13 | ```shell 14 | ./readline> cd cli 15 | ./readline/cli> go run . 16 | ``` -------------------------------------------------------------------------------- /sample/readline/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/desertbit/readline" 6 | ) 7 | 8 | func main() { 9 | if err := readline.DialRemote("tcp", ":5555"); err != nil { 10 | fmt.Errorf("An error occurred: %s \n", err.Error()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sample/readline/cmd/app.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "errors" 29 | "strings" 30 | "time" 31 | 32 | "github.com/desertbit/grumble" 33 | ) 34 | 35 | var App = grumble.New(&grumble.Config{ 36 | Name: "foo", 37 | Description: "An awesome foo bar", 38 | Flags: func(f *grumble.Flags) { 39 | f.String("d", "directory", "DEFAULT", "set an alternative root directory path") 40 | f.Bool("v", "verbose", false, "enable verbose mode") 41 | }, 42 | }) 43 | 44 | func init() { 45 | App.AddCommand(&grumble.Command{ 46 | Name: "daemon", 47 | Help: "run the daemon", 48 | Aliases: []string{"run"}, 49 | Flags: func(f *grumble.Flags) { 50 | f.Duration("t", "timeout", time.Second, "timeout duration") 51 | }, 52 | Args: func(a *grumble.Args) { 53 | a.Bool("production", "whether to start the daemon in production or development mode") 54 | a.Int("opt-level", "the optimization mode", grumble.Default(3)) 55 | a.StringList("services", "additional services that should be started", grumble.Default([]string{"test", "te11"})) 56 | }, 57 | Run: func(c *grumble.Context) error { 58 | c.App.Println("timeout:", c.Flags.Duration("timeout")) 59 | c.App.Println("directory:", c.Flags.String("directory")) 60 | c.App.Println("verbose:", c.Flags.Bool("verbose")) 61 | c.App.Println("production:", c.Args.Bool("production")) 62 | c.App.Println("opt-level:", c.Args.Int("opt-level")) 63 | c.App.Println("services:", strings.Join(c.Args.StringList("services"), ",")) 64 | return nil 65 | }, 66 | }) 67 | 68 | adminCommand := &grumble.Command{ 69 | Name: "admin", 70 | Help: "admin tools", 71 | LongHelp: "super administration tools", 72 | } 73 | App.AddCommand(adminCommand) 74 | 75 | adminCommand.AddCommand(&grumble.Command{ 76 | Name: "root", 77 | Help: "root the machine", 78 | Run: func(c *grumble.Context) error { 79 | c.App.Println(c.Flags.String("directory")) 80 | return errors.New("failed") 81 | }, 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /sample/readline/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/desertbit/grumble" 5 | "github.com/desertbit/readline" 6 | "time" 7 | ) 8 | 9 | func main() { 10 | handleFunc := func(rl *readline.Instance) { 11 | var app = grumble.New(&grumble.Config{ 12 | Name: "app", 13 | Description: "short app description", 14 | InterruptHandler: func(a *grumble.App, count int) { 15 | // do nothing 16 | }, 17 | Flags: func(f *grumble.Flags) { 18 | f.String("d", "directory", "DEFAULT", "set an alternative directory path") 19 | f.Bool("v", "verbose", false, "enable verbose mode") 20 | }, 21 | }) 22 | 23 | app.AddCommand(&grumble.Command{ 24 | Name: "daemon", 25 | Help: "run the daemon", 26 | Aliases: []string{"run"}, 27 | 28 | Flags: func(f *grumble.Flags) { 29 | f.Duration("t", "timeout", time.Second, "timeout duration") 30 | }, 31 | 32 | Args: func(a *grumble.Args) { 33 | a.String("service", "which service to start", grumble.Default("server")) 34 | }, 35 | 36 | Run: func(c *grumble.Context) error { 37 | // Parent Flags. 38 | c.App.Println("directory:", c.Flags.String("directory")) 39 | c.App.Println("verbose:", c.Flags.Bool("verbose")) 40 | // Flags. 41 | c.App.Println("timeout:", c.Flags.Duration("timeout")) 42 | // Args. 43 | c.App.Println("service:", c.Args.String("service")) 44 | return nil 45 | }, 46 | }) 47 | 48 | adminCommand := &grumble.Command{ 49 | Name: "admin", 50 | Help: "admin tools", 51 | LongHelp: "super administration tools", 52 | } 53 | app.AddCommand(adminCommand) 54 | 55 | app.RunWithReadline(rl) 56 | } 57 | 58 | cfg := &readline.Config{} 59 | readline.ListenRemote("tcp", ":5555", cfg, handleFunc) 60 | } 61 | -------------------------------------------------------------------------------- /sample/simple/cmd/app.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package cmd 26 | 27 | import ( 28 | "errors" 29 | "strings" 30 | "time" 31 | 32 | "github.com/desertbit/grumble" 33 | ) 34 | 35 | var App = grumble.New(&grumble.Config{ 36 | Name: "foo", 37 | Description: "An awesome foo bar", 38 | Flags: func(f *grumble.Flags) { 39 | f.String("d", "directory", "DEFAULT", "set an alternative root directory path") 40 | f.Bool("v", "verbose", false, "enable verbose mode") 41 | }, 42 | }) 43 | 44 | func init() { 45 | App.AddCommand(&grumble.Command{ 46 | Name: "daemon", 47 | Help: "run the daemon", 48 | Aliases: []string{"run"}, 49 | Flags: func(f *grumble.Flags) { 50 | f.Duration("t", "timeout", time.Second, "timeout duration") 51 | }, 52 | Args: func(a *grumble.Args) { 53 | a.Bool("production", "whether to start the daemon in production or development mode") 54 | a.Int("opt-level", "the optimization mode", grumble.Default(3)) 55 | a.StringList("services", "additional services that should be started", grumble.Default([]string{"test", "te11"})) 56 | }, 57 | Run: func(c *grumble.Context) error { 58 | c.App.Println("timeout:", c.Flags.Duration("timeout")) 59 | c.App.Println("directory:", c.Flags.String("directory")) 60 | c.App.Println("verbose:", c.Flags.Bool("verbose")) 61 | c.App.Println("production:", c.Args.Bool("production")) 62 | c.App.Println("opt-level:", c.Args.Int("opt-level")) 63 | c.App.Println("services:", strings.Join(c.Args.StringList("services"), ",")) 64 | return nil 65 | }, 66 | }) 67 | 68 | adminCommand := &grumble.Command{ 69 | Name: "admin", 70 | Help: "admin tools", 71 | LongHelp: "super administration tools", 72 | } 73 | App.AddCommand(adminCommand) 74 | 75 | adminCommand.AddCommand(&grumble.Command{ 76 | Name: "root", 77 | Help: "root the machine", 78 | Run: func(c *grumble.Context) error { 79 | c.App.Println(c.Flags.String("directory")) 80 | return errors.New("failed") 81 | }, 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /sample/simple/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com] 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | package main 26 | 27 | import ( 28 | "github.com/desertbit/grumble" 29 | "github.com/desertbit/grumble/sample/simple/cmd" 30 | ) 31 | 32 | func main() { 33 | grumble.Main(cmd.App) 34 | } 35 | --------------------------------------------------------------------------------