提交 891eb55a 编写于 作者: TheWayYouMakeMeFeel's avatar TheWayYouMakeMeFeel

add grumble

上级 c06bbd66
Roland Singer <roland.singer@desertbit.com>
The MIT License (MIT)
Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"fmt"
"io"
"os"
"strings"
"github.com/desertbit/closer/v3"
shlex "github.com/desertbit/go-shlex"
"github.com/desertbit/readline"
"github.com/fatih/color"
)
// App is the entrypoint.
type App struct {
closer.Closer
rl *readline.Instance
config *Config
commands Commands
isShell bool
currentPrompt string
flags Flags
flagMap FlagMap
args Args
initHook func(a *App, flags FlagMap) error
shellHook func(a *App) error
printHelp func(a *App, shell bool)
printCommandHelp func(a *App, cmd *Command, shell bool)
interruptHandler func(a *App, count int)
printASCIILogo func(a *App)
}
// New creates a new app.
// Panics if the config is invalid.
func New(c *Config) (a *App) {
// Prepare the config.
c.SetDefaults()
err := c.Validate()
if err != nil {
panic(err)
}
// APP.
a = &App{
Closer: closer.New(),
config: c,
currentPrompt: c.prompt(),
flagMap: make(FlagMap),
printHelp: defaultPrintHelp,
printCommandHelp: defaultPrintCommandHelp,
interruptHandler: defaultInterruptHandler,
}
if c.InterruptHandler != nil {
a.interruptHandler = c.InterruptHandler
}
// Register the builtin flags.
a.flags.Bool("h", "help", false, "display help")
a.flags.BoolL("nocolor", false, "disable color output")
// Register the user flags, if present.
if c.Flags != nil {
c.Flags(&a.flags)
}
return
}
// SetPrompt sets a new prompt.
func (a *App) SetPrompt(p string) {
if !a.config.NoColor {
p = a.config.PromptColor.Sprint(p)
}
a.currentPrompt = p
}
// SetDefaultPrompt resets the current prompt to the default prompt as
// configured in the config.
func (a *App) SetDefaultPrompt() {
a.currentPrompt = a.config.prompt()
}
// IsShell indicates, if this is a shell session.
func (a *App) IsShell() bool {
return a.isShell
}
// Config returns the app's config value.
func (a *App) Config() *Config {
return a.config
}
// Commands returns the app's commands.
// Access is not thread-safe. Only access during command execution.
func (a *App) Commands() *Commands {
return &a.commands
}
// PrintError prints the given error.
func (a *App) PrintError(err error) {
if a.config.NoColor {
a.Printf("error: %v\n", err)
} else {
a.config.ErrorColor.Fprint(a, "error: ")
a.Printf("%v\n", err)
}
}
// Print writes to terminal output.
// Print writes to standard output if terminal output is not yet active.
func (a *App) Print(args ...interface{}) (int, error) {
return fmt.Fprint(a, args...)
}
// Printf formats according to a format specifier and writes to terminal output.
// Printf writes to standard output if terminal output is not yet active.
func (a *App) Printf(format string, args ...interface{}) (int, error) {
return fmt.Fprintf(a, format, args...)
}
// Println writes to terminal output followed by a newline.
// Println writes to standard output if terminal output is not yet active.
func (a *App) Println(args ...interface{}) (int, error) {
return fmt.Fprintln(a, args...)
}
// OnInit sets the function which will be executed before the first command
// is executed. App flags can be handled here.
func (a *App) OnInit(f func(a *App, flags FlagMap) error) {
a.initHook = f
}
// OnShell sets the function which will be executed before the shell starts.
func (a *App) OnShell(f func(a *App) error) {
a.shellHook = f
}
// SetInterruptHandler sets the interrupt handler function.
func (a *App) SetInterruptHandler(f func(a *App, count int)) {
a.interruptHandler = f
}
// SetPrintHelp sets the print help function.
func (a *App) SetPrintHelp(f func(a *App, shell bool)) {
a.printHelp = f
}
// SetPrintCommandHelp sets the print help function for a single command.
func (a *App) SetPrintCommandHelp(f func(a *App, c *Command, shell bool)) {
a.printCommandHelp = f
}
// SetPrintASCIILogo sets the function to print the ASCII logo.
func (a *App) SetPrintASCIILogo(f func(a *App)) {
a.printASCIILogo = func(a *App) {
if !a.config.NoColor {
a.config.ASCIILogoColor.Set()
defer color.Unset()
}
f(a)
}
}
// Write to the underlying output, using readline if available.
func (a *App) Write(p []byte) (int, error) {
return a.Stdout().Write(p)
}
// Stdout returns a writer to Stdout, using readline if available.
// Note that calling before Run() will return a different instance.
func (a *App) Stdout() io.Writer {
if a.rl != nil {
return a.rl.Stdout()
}
return os.Stdout
}
// Stderr returns a writer to Stderr, using readline if available.
// Note that calling before Run() will return a different instance.
func (a *App) Stderr() io.Writer {
if a.rl != nil {
return a.rl.Stderr()
}
return os.Stderr
}
// AddCommand adds a new command.
// Panics on error.
func (a *App) AddCommand(cmd *Command) {
a.addCommand(cmd, true)
}
// addCommand adds a new command.
// If addHelpFlag is true, a help flag is automatically
// added to the command which displays its usage on use.
// Panics on error.
func (a *App) addCommand(cmd *Command, addHelpFlag bool) {
err := cmd.validate()
if err != nil {
panic(err)
}
cmd.registerFlagsAndArgs(addHelpFlag)
a.commands.Add(cmd)
}
// RunCommand runs a single command.
func (a *App) RunCommand(args []string) error {
// Parse the arguments string and obtain the command path to the root,
// and the command flags.
cmds, fg, args, err := a.commands.parse(args, a.flagMap, false)
if err != nil {
return err
} else if len(cmds) == 0 {
return fmt.Errorf("unknown command, try 'help'")
}
// The last command is the final command.
cmd := cmds[len(cmds)-1]
// Print the command help if the command run function is nil or if the help flag is set.
if fg.Bool("help") || cmd.Run == nil {
a.printCommandHelp(a, cmd, a.isShell)
return nil
}
// Parse the arguments.
cmdArgMap := make(ArgMap)
args, err = cmd.args.parse(args, cmdArgMap)
if err != nil {
return err
}
// Check, if values from the argument string are not consumed (and therefore invalid).
if len(args) > 0 {
return fmt.Errorf("invalid usage of command '%s' (unconsumed input '%s'), try 'help'", cmd.Name, strings.Join(args, " "))
}
// Create the context and pass the rest args.
ctx := newContext(a, cmd, fg, cmdArgMap)
// Run the command.
err = cmd.Run(ctx)
if err != nil {
return err
}
return nil
}
// Run the application and parse the command line arguments.
// This method blocks.
func (a *App) Run() (err error) {
// Create the readline instance.
config := &readline.Config{}
a.setReadlineDefaults(config)
rl, err := readline.NewEx(config)
if err != nil {
return err
}
return a.RunWithReadline(rl)
}
func (a *App) RunWithReadline(rl *readline.Instance) (err error) {
defer a.Close()
a.setReadlineDefaults(rl.Config)
// Sort all commands by their name.
a.commands.SortRecursive()
// Remove the program name from the args.
args := os.Args
if len(args) > 0 {
args = args[1:]
}
// Parse the app command line flags.
args, err = a.flags.parse(args, a.flagMap)
if err != nil {
return err
}
// Check if nocolor was set.
a.config.NoColor = a.flagMap.Bool("nocolor")
// Determine if this is a shell session.
a.isShell = len(args) == 0
// Add general builtin commands.
a.addCommand(&Command{
Name: "help",
Help: "use 'help [command]' for command help",
Args: func(a *Args) {
a.StringList("command", "the name of the command")
},
Run: func(c *Context) error {
args := c.Args.StringList("command")
if len(args) == 0 {
a.printHelp(a, a.isShell)
return nil
}
cmd, _, err := a.commands.FindCommand(args)
if err != nil {
return err
} else if cmd == nil {
a.PrintError(fmt.Errorf("command not found"))
return nil
}
a.printCommandHelp(a, cmd, a.isShell)
return nil
},
isBuiltin: true,
}, false)
// Check if help should be displayed.
if a.flagMap.Bool("help") {
a.printHelp(a, false)
return nil
}
// Add shell builtin commands.
// Ensure to add all commands before running the init hook.
// If the init hook does something with the app commands, then these should also be included.
if a.isShell {
a.AddCommand(&Command{
Name: "exit",
Help: "exit the shell",
Run: func(c *Context) error {
c.Stop()
return nil
},
isBuiltin: true,
})
a.AddCommand(&Command{
Name: "clear",
Help: "clear the screen",
Run: func(c *Context) error {
readline.ClearScreen(a.rl)
return nil
},
isBuiltin: true,
})
}
// Run the init hook.
if a.initHook != nil {
err = a.initHook(a, a.flagMap)
if err != nil {
return err
}
}
// Check if a command chould be executed in non-interactive mode.
if !a.isShell {
return a.RunCommand(args)
}
// Assign readline instance
a.rl = rl
a.OnClose(a.rl.Close)
// Run the shell hook.
if a.shellHook != nil {
err = a.shellHook(a)
if err != nil {
return err
}
}
// Print the ASCII logo.
if a.printASCIILogo != nil {
a.printASCIILogo(a)
}
// Run the shell.
return a.runShell()
}
func (a *App) setReadlineDefaults(config *readline.Config) {
config.Prompt = a.currentPrompt
config.HistorySearchFold = true
config.DisableAutoSaveHistory = true
config.HistoryFile = a.config.HistoryFile
config.HistoryLimit = a.config.HistoryLimit
config.AutoComplete = newCompleter(&a.commands)
config.VimMode = a.config.VimMode
}
func (a *App) runShell() error {
var interruptCount int
var lines []string
multiActive := false
Loop:
for !a.IsClosing() {
// Set the prompt.
if multiActive {
a.rl.SetPrompt(a.config.multiPrompt())
} else {
a.rl.SetPrompt(a.currentPrompt)
}
multiActive = false
// Readline.
line, err := a.rl.Readline()
if err != nil {
if err == readline.ErrInterrupt {
interruptCount++
a.interruptHandler(a, interruptCount)
continue Loop
} else if err == io.EOF {
return nil
} else {
return err
}
}
// Reset the interrupt count.
interruptCount = 0
// Handle multiline input.
if strings.HasSuffix(line, "\\") {
multiActive = true
line = strings.TrimSpace(line[:len(line)-1]) // Add without suffix and trim spaces.
lines = append(lines, line)
continue Loop
}
lines = append(lines, strings.TrimSpace(line))
line = strings.Join(lines, " ")
line = strings.TrimSpace(line)
lines = lines[:0]
// Skip if the line is empty.
if len(line) == 0 {
continue Loop
}
// Save command history.
err = a.rl.SaveHistory(line)
if err != nil {
a.PrintError(err)
continue Loop
}
// Split the line to args.
args, err := shlex.Split(line, true)
if err != nil {
a.PrintError(fmt.Errorf("invalid args: %v", err))
continue Loop
}
// Execute the command.
err = a.RunCommand(args)
if err != nil {
a.PrintError(err)
// Do not continue the Loop here. We want to handle command changes below.
}
// Sort the commands again if they have changed (Add or remove action).
if a.commands.hasChanged() {
a.commands.SortRecursive()
a.commands.unsetChanged()
}
}
return nil
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"fmt"
"time"
)
// ArgMapItem holds the specific arg data.
type ArgMapItem struct {
Value interface{}
IsDefault bool
}
// ArgMap holds all the parsed arg values.
type ArgMap map[string]*ArgMapItem
// String returns the given arg value as string.
// Panics if not present. Args must be registered.
func (a ArgMap) String(name string) string {
i := a[name]
if i == nil {
panic(fmt.Errorf("missing argument value: arg '%s' not registered", name))
}
s, ok := i.Value.(string)
if !ok {
panic(fmt.Errorf("failed to assert argument '%s' to string", name))
}
return s
}
// StringList returns the given arg value as string slice.
// Panics if not present. Args must be registered.
// If optional and not provided, nil is returned.
func (a ArgMap) StringList(long string) []string {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
if i.Value == nil {
return nil
}
s, ok := i.Value.([]string)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to string list", long))
}
return s
}
// Bool returns the given arg value as bool.
// Panics if not present. Args must be registered.
func (a ArgMap) Bool(long string) bool {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
b, ok := i.Value.(bool)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to bool", long))
}
return b
}
// BoolList returns the given arg value as bool slice.
// Panics if not present. Args must be registered.
func (a ArgMap) BoolList(long string) []bool {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
if i.Value == nil {
return nil
}
b, ok := i.Value.([]bool)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to bool list", long))
}
return b
}
// Int returns the given arg value as int.
// Panics if not present. Args must be registered.
func (a ArgMap) Int(long string) int {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
v, ok := i.Value.(int)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to int", long))
}
return v
}
// IntList returns the given arg value as int slice.
// Panics if not present. Args must be registered.
func (a ArgMap) IntList(long string) []int {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
if i.Value == nil {
return nil
}
v, ok := i.Value.([]int)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to int list", long))
}
return v
}
// Int64 returns the given arg value as int64.
// Panics if not present. Args must be registered.
func (a ArgMap) Int64(long string) int64 {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
v, ok := i.Value.(int64)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to int64", long))
}
return v
}
// Int64List returns the given arg value as int64.
// Panics if not present. Args must be registered.
func (a ArgMap) Int64List(long string) []int64 {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
if i.Value == nil {
return nil
}
v, ok := i.Value.([]int64)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to int64 list", long))
}
return v
}
// Uint returns the given arg value as uint.
// Panics if not present. Args must be registered.
func (a ArgMap) Uint(long string) uint {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
v, ok := i.Value.(uint)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to uint", long))
}
return v
}
// UintList returns the given arg value as uint.
// Panics if not present. Args must be registered.
func (a ArgMap) UintList(long string) []uint {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
if i.Value == nil {
return nil
}
v, ok := i.Value.([]uint)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to uint list", long))
}
return v
}
// Uint64 returns the given arg value as uint64.
// Panics if not present. Args must be registered.
func (a ArgMap) Uint64(long string) uint64 {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
v, ok := i.Value.(uint64)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to uint64", long))
}
return v
}
// Uint64List returns the given arg value as uint64.
// Panics if not present. Args must be registered.
func (a ArgMap) Uint64List(long string) []uint64 {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
if i.Value == nil {
return nil
}
v, ok := i.Value.([]uint64)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to uint64 list", long))
}
return v
}
// Float64 returns the given arg value as float64.
// Panics if not present. Args must be registered.
func (a ArgMap) Float64(long string) float64 {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
v, ok := i.Value.(float64)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to float64", long))
}
return v
}
// Float64List returns the given arg value as float64.
// Panics if not present. Args must be registered.
func (a ArgMap) Float64List(long string) []float64 {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
if i.Value == nil {
return nil
}
v, ok := i.Value.([]float64)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to float64 list", long))
}
return v
}
// Duration returns the given arg value as duration.
// Panics if not present. Args must be registered.
func (a ArgMap) Duration(long string) time.Duration {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
v, ok := i.Value.(time.Duration)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to duration", long))
}
return v
}
// DurationList returns the given arg value as duration.
// Panics if not present. Args must be registered.
func (a ArgMap) DurationList(long string) []time.Duration {
i := a[long]
if i == nil {
panic(fmt.Errorf("missing arg value: arg '%s' not registered", long))
}
if i.Value == nil {
return nil
}
v, ok := i.Value.([]time.Duration)
if !ok {
panic(fmt.Errorf("failed to assert arg '%s' to duration list", long))
}
return v
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
// ArgOption can be supplied to modify an argument.
type ArgOption func(*argItem)
// Min sets the minimum required number of elements for a list argument.
func Min(m int) ArgOption {
if m < 0 {
panic("min must be >= 0")
}
return func(i *argItem) {
if !i.isList {
panic("min option only valid for list arguments")
}
i.listMin = m
}
}
// Max sets the maximum required number of elements for a list argument.
func Max(m int) ArgOption {
if m < 1 {
panic("max must be >= 1")
}
return func(i *argItem) {
if !i.isList {
panic("max option only valid for list arguments")
}
i.listMax = m
}
}
// Default sets a default value for the argument.
// The argument becomes optional then.
func Default(v interface{}) ArgOption {
if v == nil {
panic("nil default value not allowed")
}
return func(i *argItem) {
i.Default = v
i.optional = true
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"fmt"
"strconv"
"time"
)
// The parseArgFunc describes a func that parses from the given command line arguments
// the values for its argument and saves them to the ArgMap.
// It returns the not-consumed arguments and an error.
type parseArgFunc func(args []string, res ArgMap) ([]string, error)
type argItem struct {
Name string
Help string
HelpArgs string
Default interface{}
parser parseArgFunc
isList bool
optional bool
listMin int
listMax int
}
// Args holds all the registered args.
type Args struct {
list []*argItem
}
func (a *Args) register(
name, help, helpArgs string,
isList bool,
pf parseArgFunc,
opts ...ArgOption,
) {
// Validate.
if name == "" {
panic("empty argument name")
} else if help == "" {
panic(fmt.Errorf("missing help message for argument '%s'", name))
}
// Ensure the name is unique.
for _, ai := range a.list {
if ai.Name == name {
panic(fmt.Errorf("argument '%s' registered twice", name))
}
}
// Create the item.
item := &argItem{
Name: name,
Help: help,
HelpArgs: helpArgs,
parser: pf,
isList: isList,
optional: isList,
listMin: -1,
listMax: -1,
}
// Apply options.
// Afterwards, we can make some final checks.
for _, opt := range opts {
opt(item)
}
if item.isList && item.listMax > 0 && item.listMax < item.listMin {
panic("max must not be less than min for list arguments")
}
if !a.empty() {
last := a.list[len(a.list)-1]
// Check, if a list argument has been supplied already.
if last.isList {
panic("list argument has been registered, nothing can come after it")
}
// Check, that after an optional argument no mandatory one follows.
if !item.optional && last.optional {
panic("mandatory argument not allowed after optional one")
}
}
a.list = append(a.list, item)
}
// empty returns true, if the args are empty.
func (a *Args) empty() bool {
return len(a.list) == 0
}
func (a *Args) parse(args []string, res ArgMap) ([]string, error) {
// Iterate over all arguments that have been registered.
// There must be either a default value or a value available,
// otherwise the argument is missing.
var err error
for _, item := range a.list {
// If it is a list argument, it will consume the rest of the input.
// Check that it matches its range.
if item.isList {
if len(args) < item.listMin {
return nil, fmt.Errorf("argument '%s' requires at least %d element(s)", item.Name, item.listMin)
}
if item.listMax > 0 && len(args) > item.listMax {
return nil, fmt.Errorf("argument '%s' requires at most %d element(s)", item.Name, item.listMax)
}
}
// If no arguments are left, simply set the default values.
if len(args) == 0 {
// Check, if the argument is mandatory.
if !item.optional {
return nil, fmt.Errorf("missing argument '%s'", item.Name)
}
// Register its default value.
res[item.Name] = &ArgMapItem{Value: item.Default, IsDefault: true}
continue
}
args, err = item.parser(args, res)
if err != nil {
return nil, err
}
}
return args, nil
}
// String registers a string argument.
func (a *Args) String(name, help string, opts ...ArgOption) {
a.register(name, help, "string", false,
func(args []string, res ArgMap) ([]string, error) {
res[name] = &ArgMapItem{Value: args[0]}
return args[1:], nil
},
opts...,
)
}
// StringList registers a string list argument.
func (a *Args) StringList(name, help string, opts ...ArgOption) {
a.register(name, help, "string list", true,
func(args []string, res ArgMap) ([]string, error) {
res[name] = &ArgMapItem{Value: args}
return []string{}, nil
},
opts...,
)
}
// Bool registers a bool argument.
func (a *Args) Bool(name, help string, opts ...ArgOption) {
a.register(name, help, "bool", false,
func(args []string, res ArgMap) ([]string, error) {
b, err := strconv.ParseBool(args[0])
if err != nil {
return nil, fmt.Errorf("invalid bool value '%s' for argument: %s", args[0], name)
}
res[name] = &ArgMapItem{Value: b}
return args[1:], nil
},
opts...,
)
}
// BoolList registers a bool list argument.
func (a *Args) BoolList(name, help string, opts ...ArgOption) {
a.register(name, help, "bool list", true,
func(args []string, res ArgMap) ([]string, error) {
var (
err error
bs = make([]bool, len(args))
)
for i, a := range args {
bs[i], err = strconv.ParseBool(a)
if err != nil {
return nil, fmt.Errorf("invalid bool value '%s' for argument: %s", a, name)
}
}
res[name] = &ArgMapItem{Value: bs}
return []string{}, nil
},
opts...,
)
}
// Int registers an int argument.
func (a *Args) Int(name, help string, opts ...ArgOption) {
a.register(name, help, "int", false,
func(args []string, res ArgMap) ([]string, error) {
i, err := strconv.Atoi(args[0])
if err != nil {
return nil, fmt.Errorf("invalid int value '%s' for argument: %s", args[0], name)
}
res[name] = &ArgMapItem{Value: i}
return args[1:], nil
},
opts...,
)
}
// IntList registers an int list argument.
func (a *Args) IntList(name, help string, opts ...ArgOption) {
a.register(name, help, "int list", true,
func(args []string, res ArgMap) ([]string, error) {
var (
err error
is = make([]int, len(args))
)
for i, a := range args {
is[i], err = strconv.Atoi(a)
if err != nil {
return nil, fmt.Errorf("invalid int value '%s' for argument: %s", a, name)
}
}
res[name] = &ArgMapItem{Value: is}
return []string{}, nil
},
opts...,
)
}
// Int64 registers an int64 argument.
func (a *Args) Int64(name, help string, opts ...ArgOption) {
a.register(name, help, "int64", false,
func(args []string, res ArgMap) ([]string, error) {
i, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid int64 value '%s' for argument: %s", args[0], name)
}
res[name] = &ArgMapItem{Value: i}
return args[1:], nil
},
opts...,
)
}
// Int64List registers an int64 list argument.
func (a *Args) Int64List(name, help string, opts ...ArgOption) {
a.register(name, help, "int64 list", true,
func(args []string, res ArgMap) ([]string, error) {
var (
err error
is = make([]int64, len(args))
)
for i, a := range args {
is[i], err = strconv.ParseInt(a, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid int64 value '%s' for argument: %s", a, name)
}
}
res[name] = &ArgMapItem{Value: is}
return []string{}, nil
},
opts...,
)
}
// Uint registers an uint argument.
func (a *Args) Uint(name, help string, opts ...ArgOption) {
a.register(name, help, "uint", false,
func(args []string, res ArgMap) ([]string, error) {
u, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid uint value '%s' for argument: %s", args[0], name)
}
res[name] = &ArgMapItem{Value: uint(u)}
return args[1:], nil
},
opts...,
)
}
// UintList registers an uint list argument.
func (a *Args) UintList(name, help string, opts ...ArgOption) {
a.register(name, help, "uint list", true,
func(args []string, res ArgMap) ([]string, error) {
var (
err error
u uint64
is = make([]uint, len(args))
)
for i, a := range args {
u, err = strconv.ParseUint(a, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid uint value '%s' for argument: %s", a, name)
}
is[i] = uint(u)
}
res[name] = &ArgMapItem{Value: is}
return []string{}, nil
},
opts...,
)
}
// Uint64 registers an uint64 argument.
func (a *Args) Uint64(name, help string, opts ...ArgOption) {
a.register(name, help, "uint64", false,
func(args []string, res ArgMap) ([]string, error) {
u, err := strconv.ParseUint(args[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid uint64 value '%s' for argument: %s", args[0], name)
}
res[name] = &ArgMapItem{Value: u}
return args[1:], nil
},
opts...,
)
}
// Uint64List registers an uint64 list argument.
func (a *Args) Uint64List(name, help string, opts ...ArgOption) {
a.register(name, help, "uint64 list", true,
func(args []string, res ArgMap) ([]string, error) {
var (
err error
us = make([]uint64, len(args))
)
for i, a := range args {
us[i], err = strconv.ParseUint(a, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid uint64 value '%s' for argument: %s", a, name)
}
}
res[name] = &ArgMapItem{Value: us}
return []string{}, nil
},
opts...,
)
}
// Float64 registers a float64 argument.
func (a *Args) Float64(name, help string, opts ...ArgOption) {
a.register(name, help, "float64", false,
func(args []string, res ArgMap) ([]string, error) {
f, err := strconv.ParseFloat(args[0], 64)
if err != nil {
return nil, fmt.Errorf("invalid float64 value '%s' for argument: %s", args[0], name)
}
res[name] = &ArgMapItem{Value: f}
return args[1:], nil
},
opts...,
)
}
// Float64List registers an float64 list argument.
func (a *Args) Float64List(name, help string, opts ...ArgOption) {
a.register(name, help, "float64 list", true,
func(args []string, res ArgMap) ([]string, error) {
var (
err error
fs = make([]float64, len(args))
)
for i, a := range args {
fs[i], err = strconv.ParseFloat(a, 64)
if err != nil {
return nil, fmt.Errorf("invalid float64 value '%s' for argument: %s", a, name)
}
}
res[name] = &ArgMapItem{Value: fs}
return []string{}, nil
},
opts...,
)
}
// Duration registers a duration argument.
func (a *Args) Duration(name, help string, opts ...ArgOption) {
a.register(name, help, "duration", false,
func(args []string, res ArgMap) ([]string, error) {
d, err := time.ParseDuration(args[0])
if err != nil {
return nil, fmt.Errorf("invalid duration value '%s' for argument: %s", args[0], name)
}
res[name] = &ArgMapItem{Value: d}
return args[1:], nil
},
opts...,
)
}
// DurationList registers an duration list argument.
func (a *Args) DurationList(name, help string, opts ...ArgOption) {
a.register(name, help, "duration list", true,
func(args []string, res ArgMap) ([]string, error) {
var (
err error
ds = make([]time.Duration, len(args))
)
for i, a := range args {
ds[i], err = time.ParseDuration(a)
if err != nil {
return nil, fmt.Errorf("invalid duration value '%s' for argument: %s", a, name)
}
}
res[name] = &ArgMapItem{Value: ds}
return []string{}, nil
},
opts...,
)
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"fmt"
)
// Command is just that, a command for your application.
type Command struct {
// Command name.
// This field is required.
Name string
// Command name aliases.
Aliases []string
// One liner help message for the command.
// This field is required.
Help string
// More descriptive help message for the command.
LongHelp string
// HelpGroup defines the help group headline.
// Note: this is only used for primary top-level commands.
HelpGroup string
// Usage should define how to use the command.
// Sample: start [OPTIONS] CONTAINER [CONTAINER...]
Usage string
// Define all command flags within this function.
Flags func(f *Flags)
// Define all command arguments within this function.
Args func(a *Args)
// Function to execute for the command.
Run func(c *Context) error
// Completer is custom autocompleter for command.
// It takes in command arguments and returns autocomplete options.
// By default all commands get autocomplete of subcommands.
// A non-nil Completer overrides the default behaviour.
Completer func(prefix string, args []string) []string
parent *Command
flags Flags
args Args
commands Commands
isBuiltin bool // Whenever this is a build-in command not added by the user.
}
func (c *Command) validate() error {
if len(c.Name) == 0 {
return fmt.Errorf("empty command name")
} else if c.Name[0] == '-' {
return fmt.Errorf("command name must not start with a '-'")
} else if len(c.Help) == 0 {
return fmt.Errorf("empty command help")
}
return nil
}
func (c *Command) registerFlagsAndArgs(addHelpFlag bool) {
if addHelpFlag {
// Add default help command.
c.flags.Bool("h", "help", false, "display help")
}
if c.Flags != nil {
c.Flags(&c.flags)
}
if c.Args != nil {
c.Args(&c.args)
}
}
// Parent returns the parent command or nil.
func (c *Command) Parent() *Command {
return c.parent
}
// AddCommand adds a new command.
// Panics on error.
func (c *Command) AddCommand(cmd *Command) {
err := cmd.validate()
if err != nil {
panic(err)
}
cmd.parent = c
cmd.registerFlagsAndArgs(true)
c.commands.Add(cmd)
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"sort"
)
// Commands collection.
type Commands struct {
list []*Command
changed bool // Used to resort if something changes.
}
// Add the command to the slice.
// Duplicates are ignored.
func (c *Commands) Add(cmd *Command) {
c.list = append(c.list, cmd)
c.changed = true
}
// Remove a command from the slice.
func (c *Commands) Remove(name string) (found bool) {
for index, cmd := range c.list {
if cmd.Name == name {
found = true
c.changed = true
c.list = append(c.list[:index], c.list[index+1:]...)
return
}
}
return
}
func (c *Commands) RemoveAll() {
var builtins []*Command
// Hint: There are no built-in sub commands. Ignore them.
for _, cmd := range c.list {
if cmd.isBuiltin {
builtins = append(builtins, cmd)
}
}
// Only keep the builtins.
c.list = builtins
c.changed = true
}
// All returns a slice of all commands.
func (c *Commands) All() []*Command {
return c.list
}
// Get the command by the name. Aliases are also checked.
// Returns nil if not found.
func (c *Commands) Get(name string) *Command {
for _, cmd := range c.list {
if cmd.Name == name {
return cmd
}
for _, a := range cmd.Aliases {
if a == name {
return cmd
}
}
}
return nil
}
// FindCommand searches for the final command through all children.
// Returns a slice of non processed following command args.
// Returns cmd=nil if not found.
func (c *Commands) FindCommand(args []string) (cmd *Command, rest []string, err error) {
var cmds []*Command
cmds, _, rest, err = c.parse(args, nil, true)
if err != nil {
return
}
if len(cmds) > 0 {
cmd = cmds[len(cmds)-1]
}
return
}
// Sort the commands by their name.
func (c *Commands) Sort() {
sort.Slice(c.list, func(i, j int) bool {
return c.list[i].Name < c.list[j].Name
})
}
// SortRecursive sorts the commands by their name including all sub commands.
func (c *Commands) SortRecursive() {
c.Sort()
for _, cmd := range c.list {
cmd.commands.SortRecursive()
}
}
func (c *Commands) hasChanged() bool {
if c.changed {
return true
}
for _, sc := range c.list {
if sc.commands.hasChanged() {
return true
}
}
return false
}
func (c *Commands) unsetChanged() {
c.changed = false
for _, sc := range c.list {
sc.commands.unsetChanged()
}
}
// parse the args and return a command path to the root.
// cmds slice is empty, if no command was found.
func (c *Commands) parse(
args []string,
parentFlagMap FlagMap,
skipFlagMaps bool,
) (
cmds []*Command,
flagsMap FlagMap,
rest []string,
err error,
) {
var fgs []FlagMap
cur := c
for len(args) > 0 && cur != nil {
// Extract the command name from the arguments.
name := args[0]
// Try to find the command.
cmd := cur.Get(name)
if cmd == nil {
break
}
args = args[1:]
cmds = append(cmds, cmd)
cur = &cmd.commands
// Parse the command flags.
fg := make(FlagMap)
args, err = cmd.flags.parse(args, fg)
if err != nil {
return
}
if !skipFlagMaps {
fgs = append(fgs, fg)
}
}
if !skipFlagMaps {
// Merge all the flag maps without default values.
flagsMap = make(FlagMap)
for i := len(fgs) - 1; i >= 0; i-- {
flagsMap.copyMissingValues(fgs[i], false)
}
flagsMap.copyMissingValues(parentFlagMap, false)
// Now include default values. This will ensure, that default values have
// lower rank.
for i := len(fgs) - 1; i >= 0; i-- {
flagsMap.copyMissingValues(fgs[i], true)
}
flagsMap.copyMissingValues(parentFlagMap, true)
}
rest = args
return
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"strings"
shlex "github.com/desertbit/go-shlex"
)
type completer struct {
commands *Commands
}
func newCompleter(commands *Commands) *completer {
return &completer{
commands: commands,
}
}
func (c *completer) Do(line []rune, pos int) (newLine [][]rune, length int) {
// Discard anything after the cursor position.
// This is similar behaviour to shell/bash.
line = line[:pos]
var words []string
if w, err := shlex.Split(string(line), true); err == nil {
words = w
} else {
words = strings.Fields(string(line)) // fallback
}
prefix := ""
if len(words) > 0 && pos >= 1 && line[pos-1] != ' ' {
prefix = words[len(words)-1]
words = words[:len(words)-1]
}
// Simple hack to allow auto completion for help.
if len(words) > 0 && words[0] == "help" {
words = words[1:]
}
var (
cmds *Commands
flags *Flags
suggestions [][]rune
)
// Find the last commands list.
if len(words) == 0 {
cmds = c.commands
} else {
cmd, rest, err := c.commands.FindCommand(words)
if err != nil || cmd == nil {
return
}
// Call the custom completer if present.
if cmd.Completer != nil {
words = cmd.Completer(prefix, rest)
for _, w := range words {
suggestions = append(suggestions, []rune(strings.TrimPrefix(w, prefix)))
}
return suggestions, len(prefix)
}
// No rest must be there.
if len(rest) != 0 {
return
}
cmds = &cmd.commands
flags = &cmd.flags
}
if len(prefix) > 0 {
for _, cmd := range cmds.list {
if strings.HasPrefix(cmd.Name, prefix) {
suggestions = append(suggestions, []rune(strings.TrimPrefix(cmd.Name, prefix)))
}
for _, a := range cmd.Aliases {
if strings.HasPrefix(a, prefix) {
suggestions = append(suggestions, []rune(strings.TrimPrefix(a, prefix)))
}
}
}
if flags != nil {
for _, f := range flags.list {
if len(f.Short) > 0 {
short := "-" + f.Short
if len(prefix) < len(short) && strings.HasPrefix(short, prefix) {
suggestions = append(suggestions, []rune(strings.TrimPrefix(short, prefix)))
}
}
long := "--" + f.Long
if len(prefix) < len(long) && strings.HasPrefix(long, prefix) {
suggestions = append(suggestions, []rune(strings.TrimPrefix(long, prefix)))
}
}
}
} else {
for _, cmd := range cmds.list {
suggestions = append(suggestions, []rune(cmd.Name))
}
if flags != nil {
for _, f := range flags.list {
suggestions = append(suggestions, []rune("--"+f.Long))
if len(f.Short) > 0 {
suggestions = append(suggestions, []rune("-"+f.Short))
}
}
}
}
// Append an empty space to each suggestions.
for i, s := range suggestions {
suggestions[i] = append(s, ' ')
}
return suggestions, len(prefix)
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"fmt"
"github.com/fatih/color"
)
const (
defaultMultiPrompt = "... "
)
// Config specifies the application options.
type Config struct {
// Name specifies the application name. This field is required.
Name string
// Description specifies the application description.
Description string
// Define all app command flags within this function.
Flags func(f *Flags)
// Persist readline historys to file if specified.
HistoryFile string
// Specify the max length of historys, it's 500 by default, set it to -1 to disable history.
HistoryLimit int
// NoColor defines if color output should be disabled.
NoColor bool
// VimMode defines if Readline is to use VimMode for line navigation.
VimMode bool
// Prompt defines the shell prompt.
Prompt string
PromptColor *color.Color
// MultiPrompt defines the prompt shown on multi readline.
MultiPrompt string
MultiPromptColor *color.Color
// Some more optional color settings.
ASCIILogoColor *color.Color
ErrorColor *color.Color
// Help styling.
HelpHeadlineUnderline bool
HelpSubCommands bool
HelpHeadlineColor *color.Color
// Override default iterrupt handler
InterruptHandler func(a *App, count int)
}
// SetDefaults sets the default values if not set.
func (c *Config) SetDefaults() {
if c.HistoryLimit == 0 {
c.HistoryLimit = 500
}
if c.PromptColor == nil {
c.PromptColor = color.New(color.FgYellow, color.Bold)
}
if len(c.Prompt) == 0 {
c.Prompt = c.Name + " » "
}
if c.MultiPromptColor == nil {
c.MultiPromptColor = c.PromptColor
}
if len(c.MultiPrompt) == 0 {
c.MultiPrompt = defaultMultiPrompt
}
if c.ASCIILogoColor == nil {
c.ASCIILogoColor = c.PromptColor
}
if c.ErrorColor == nil {
c.ErrorColor = color.New(color.FgRed, color.Bold)
}
}
// Validate the required config fields.
func (c *Config) Validate() error {
if len(c.Name) == 0 {
return fmt.Errorf("application name is not set")
}
return nil
}
func (c *Config) prompt() string {
if c.NoColor {
return c.Prompt
}
return c.PromptColor.Sprint(c.Prompt)
}
func (c *Config) multiPrompt() string {
if c.NoColor {
return c.MultiPrompt
}
return c.MultiPromptColor.Sprint(c.MultiPrompt)
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
// Context defines a command context.
type Context struct {
// Reference to the app.
App *App
// Flags contains all command line flags.
Flags FlagMap
// Args contains all command line arguments.
Args ArgMap
// Cmd is the currently executing command.
Command *Command
}
func newContext(a *App, cmd *Command, flags FlagMap, args ArgMap) *Context {
return &Context{
App: a,
Command: cmd,
Flags: flags,
Args: args,
}
}
// Stop signalizes the app to exit.
func (c *Context) Stop() {
_ = c.App.Close()
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"fmt"
"time"
)
// FlagMapItem holds the specific flag data.
type FlagMapItem struct {
Value interface{}
IsDefault bool
}
// FlagMap holds all the parsed flag values.
type FlagMap map[string]*FlagMapItem
// copyMissingValues adds all missing values to the flags map.
func (f FlagMap) copyMissingValues(m FlagMap, copyDefault bool) {
for k, v := range m {
if _, ok := f[k]; !ok {
if !copyDefault && v.IsDefault {
continue
}
f[k] = v
}
}
}
// String returns the given flag value as string.
// Panics if not present. Flags must be registered.
func (f FlagMap) String(long string) string {
i := f[long]
if i == nil {
panic(fmt.Errorf("missing flag value: flag '%s' not registered", long))
}
s, ok := i.Value.(string)
if !ok {
panic(fmt.Errorf("failed to assert flag '%s' to string", long))
}
return s
}
// Bool returns the given flag value as boolean.
// Panics if not present. Flags must be registered.
func (f FlagMap) Bool(long string) bool {
i := f[long]
if i == nil {
panic(fmt.Errorf("missing flag value: flag '%s' not registered", long))
}
b, ok := i.Value.(bool)
if !ok {
panic(fmt.Errorf("failed to assert flag '%s' to bool", long))
}
return b
}
// Int returns the given flag value as int.
// Panics if not present. Flags must be registered.
func (f FlagMap) Int(long string) int {
i := f[long]
if i == nil {
panic(fmt.Errorf("missing flag value: flag '%s' not registered", long))
}
v, ok := i.Value.(int)
if !ok {
panic(fmt.Errorf("failed to assert flag '%s' to int", long))
}
return v
}
// Int64 returns the given flag value as int64.
// Panics if not present. Flags must be registered.
func (f FlagMap) Int64(long string) int64 {
i := f[long]
if i == nil {
panic(fmt.Errorf("missing flag value: flag '%s' not registered", long))
}
v, ok := i.Value.(int64)
if !ok {
panic(fmt.Errorf("failed to assert flag '%s' to int64", long))
}
return v
}
// Uint returns the given flag value as uint.
// Panics if not present. Flags must be registered.
func (f FlagMap) Uint(long string) uint {
i := f[long]
if i == nil {
panic(fmt.Errorf("missing flag value: flag '%s' not registered", long))
}
v, ok := i.Value.(uint)
if !ok {
panic(fmt.Errorf("failed to assert flag '%s' to uint", long))
}
return v
}
// Uint64 returns the given flag value as uint64.
// Panics if not present. Flags must be registered.
func (f FlagMap) Uint64(long string) uint64 {
i := f[long]
if i == nil {
panic(fmt.Errorf("missing flag value: flag '%s' not registered", long))
}
v, ok := i.Value.(uint64)
if !ok {
panic(fmt.Errorf("failed to assert flag '%s' to uint64", long))
}
return v
}
// Float64 returns the given flag value as float64.
// Panics if not present. Flags must be registered.
func (f FlagMap) Float64(long string) float64 {
i := f[long]
if i == nil {
panic(fmt.Errorf("missing flag value: flag '%s' not registered", long))
}
v, ok := i.Value.(float64)
if !ok {
panic(fmt.Errorf("failed to assert flag '%s' to float64", long))
}
return v
}
// Duration returns the given flag value as duration.
// Panics if not present. Flags must be registered.
func (f FlagMap) Duration(long string) time.Duration {
i := f[long]
if i == nil {
panic(fmt.Errorf("missing flag value: flag '%s' not registered", long))
}
v, ok := i.Value.(time.Duration)
if !ok {
panic(fmt.Errorf("failed to assert flag '%s' to duration", long))
}
return v
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
)
type parseFlagFunc func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error)
type defaultFlagFunc func(res FlagMap)
type flagItem struct {
Short string
Long string
Help string
HelpArgs string
HelpShowDefault bool
Default interface{}
}
// Flags holds all the registered flags.
type Flags struct {
parsers []parseFlagFunc
defaults map[string]defaultFlagFunc
list []*flagItem
}
// empty returns true, if the flags are empty.
func (f *Flags) empty() bool {
return len(f.list) == 0
}
// sort the flags by their name.
func (f *Flags) sort() {
sort.Slice(f.list, func(i, j int) bool {
return f.list[i].Long < f.list[j].Long
})
}
func (f *Flags) register(
short, long, help, helpArgs string,
helpShowDefault bool,
defaultValue interface{},
df defaultFlagFunc,
pf parseFlagFunc,
) {
// Validate.
if len(short) > 1 {
panic(fmt.Errorf("invalid short flag: '%s': must be a single character", short))
} else if strings.HasPrefix(short, "-") {
panic(fmt.Errorf("invalid short flag: '%s': must not start with a '-'", short))
} else if len(long) == 0 {
panic(fmt.Errorf("empty long flag: short='%s'", short))
} else if strings.HasPrefix(long, "-") {
panic(fmt.Errorf("invalid long flag: '%s': must not start with a '-'", long))
} else if len(help) == 0 {
panic(fmt.Errorf("empty flag help message for flag: '%s'", long))
}
// Check, that both short and long are unique.
// Short flags are empty if not set.
for _, fi := range f.list {
if fi.Short != "" && short != "" && fi.Short == short {
panic(fmt.Errorf("flag shortcut '%s' registered twice", short))
}
if fi.Long == long {
panic(fmt.Errorf("flag '%s' registered twice", long))
}
}
f.list = append(f.list, &flagItem{
Short: short,
Long: long,
Help: help,
HelpShowDefault: helpShowDefault,
HelpArgs: helpArgs,
Default: defaultValue,
})
if f.defaults == nil {
f.defaults = make(map[string]defaultFlagFunc)
}
f.defaults[long] = df
f.parsers = append(f.parsers, pf)
}
func (f *Flags) match(flag, short, long string) bool {
return (len(short) > 0 && flag == "-"+short) ||
(len(long) > 0 && flag == "--"+long)
}
func (f *Flags) parse(args []string, res FlagMap) ([]string, error) {
var err error
var parsed bool
// Parse all leading flags.
Loop:
for len(args) > 0 {
a := args[0]
if !strings.HasPrefix(a, "-") {
break Loop
}
args = args[1:]
// A double dash (--) is used to signify the end of command options,
// after which only positional arguments are accepted.
if a == "--" {
break Loop
}
pos := strings.Index(a, "=")
equalVal := ""
if pos > 0 {
equalVal = a[pos+1:]
a = a[:pos]
}
for _, p := range f.parsers {
args, parsed, err = p(a, equalVal, args, res)
if err != nil {
return nil, err
} else if parsed {
continue Loop
}
}
return nil, fmt.Errorf("invalid flag: %s", a)
}
// Finally set all the default values for not passed flags.
if f.defaults == nil {
return args, nil
}
for _, i := range f.list {
if _, ok := res[i.Long]; ok {
continue
}
df, ok := f.defaults[i.Long]
if !ok {
return nil, fmt.Errorf("invalid flag: missing default function: %s", i.Long)
}
df(res)
}
return args, nil
}
// StringL same as String, but without a shorthand.
func (f *Flags) StringL(long, defaultValue, help string) {
f.String("", long, defaultValue, help)
}
// String registers a string flag.
func (f *Flags) String(short, long, defaultValue, help string) {
f.register(short, long, help, "string", true, defaultValue,
func(res FlagMap) {
res[long] = &FlagMapItem{
Value: defaultValue,
IsDefault: true,
}
},
func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) {
if !f.match(flag, short, long) {
return args, false, nil
}
if len(equalVal) > 0 {
res[long] = &FlagMapItem{
Value: trimQuotes(equalVal),
IsDefault: false,
}
return args, true, nil
}
if len(args) == 0 {
return args, false, fmt.Errorf("missing string value for flag: %s", flag)
}
res[long] = &FlagMapItem{
Value: args[0],
IsDefault: false,
}
args = args[1:]
return args, true, nil
})
}
// BoolL same as Bool, but without a shorthand.
func (f *Flags) BoolL(long string, defaultValue bool, help string) {
f.Bool("", long, defaultValue, help)
}
// Bool registers a boolean flag.
func (f *Flags) Bool(short, long string, defaultValue bool, help string) {
f.register(short, long, help, "", false, defaultValue,
func(res FlagMap) {
res[long] = &FlagMapItem{
Value: defaultValue,
IsDefault: true,
}
},
func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) {
if !f.match(flag, short, long) {
return args, false, nil
}
if len(equalVal) > 0 {
b, err := strconv.ParseBool(equalVal)
if err != nil {
return args, false, fmt.Errorf("invalid boolean value for flag: %s", flag)
}
res[long] = &FlagMapItem{
Value: b,
IsDefault: false,
}
return args, true, nil
}
res[long] = &FlagMapItem{
Value: true,
IsDefault: false,
}
return args, true, nil
})
}
// IntL same as Int, but without a shorthand.
func (f *Flags) IntL(long string, defaultValue int, help string) {
f.Int("", long, defaultValue, help)
}
// Int registers an int flag.
func (f *Flags) Int(short, long string, defaultValue int, help string) {
f.register(short, long, help, "int", true, defaultValue,
func(res FlagMap) {
res[long] = &FlagMapItem{
Value: defaultValue,
IsDefault: true,
}
},
func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) {
if !f.match(flag, short, long) {
return args, false, nil
}
var vStr string
if len(equalVal) > 0 {
vStr = equalVal
} else if len(args) > 0 {
vStr = args[0]
args = args[1:]
} else {
return args, false, fmt.Errorf("missing int value for flag: %s", flag)
}
i, err := strconv.Atoi(vStr)
if err != nil {
return args, false, fmt.Errorf("invalid int value for flag: %s", flag)
}
res[long] = &FlagMapItem{
Value: i,
IsDefault: false,
}
return args, true, nil
})
}
// Int64L same as Int64, but without a shorthand.
func (f *Flags) Int64L(long string, defaultValue int64, help string) {
f.Int64("", long, defaultValue, help)
}
// Int64 registers an int64 flag.
func (f *Flags) Int64(short, long string, defaultValue int64, help string) {
f.register(short, long, help, "int", true, defaultValue,
func(res FlagMap) {
res[long] = &FlagMapItem{
Value: defaultValue,
IsDefault: true,
}
},
func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) {
if !f.match(flag, short, long) {
return args, false, nil
}
var vStr string
if len(equalVal) > 0 {
vStr = equalVal
} else if len(args) > 0 {
vStr = args[0]
args = args[1:]
} else {
return args, false, fmt.Errorf("missing int value for flag: %s", flag)
}
i, err := strconv.ParseInt(vStr, 10, 64)
if err != nil {
return args, false, fmt.Errorf("invalid int value for flag: %s", flag)
}
res[long] = &FlagMapItem{
Value: i,
IsDefault: false,
}
return args, true, nil
})
}
// UintL same as Uint, but without a shorthand.
func (f *Flags) UintL(long string, defaultValue uint, help string) {
f.Uint("", long, defaultValue, help)
}
// Uint registers an uint flag.
func (f *Flags) Uint(short, long string, defaultValue uint, help string) {
f.register(short, long, help, "uint", true, defaultValue,
func(res FlagMap) {
res[long] = &FlagMapItem{
Value: defaultValue,
IsDefault: true,
}
},
func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) {
if !f.match(flag, short, long) {
return args, false, nil
}
var vStr string
if len(equalVal) > 0 {
vStr = equalVal
} else if len(args) > 0 {
vStr = args[0]
args = args[1:]
} else {
return args, false, fmt.Errorf("missing uint value for flag: %s", flag)
}
i, err := strconv.ParseUint(vStr, 10, 64)
if err != nil {
return args, false, fmt.Errorf("invalid uint value for flag: %s", flag)
}
res[long] = &FlagMapItem{
Value: uint(i),
IsDefault: false,
}
return args, true, nil
})
}
// Uint64L same as Uint64, but without a shorthand.
func (f *Flags) Uint64L(long string, defaultValue uint64, help string) {
f.Uint64("", long, defaultValue, help)
}
// Uint64 registers an uint64 flag.
func (f *Flags) Uint64(short, long string, defaultValue uint64, help string) {
f.register(short, long, help, "uint", true, defaultValue,
func(res FlagMap) {
res[long] = &FlagMapItem{
Value: defaultValue,
IsDefault: true,
}
},
func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) {
if !f.match(flag, short, long) {
return args, false, nil
}
var vStr string
if len(equalVal) > 0 {
vStr = equalVal
} else if len(args) > 0 {
vStr = args[0]
args = args[1:]
} else {
return args, false, fmt.Errorf("missing uint value for flag: %s", flag)
}
i, err := strconv.ParseUint(vStr, 10, 64)
if err != nil {
return args, false, fmt.Errorf("invalid uint value for flag: %s", flag)
}
res[long] = &FlagMapItem{
Value: i,
IsDefault: false,
}
return args, true, nil
})
}
// Float64L same as Float64, but without a shorthand.
func (f *Flags) Float64L(long string, defaultValue float64, help string) {
f.Float64("", long, defaultValue, help)
}
// Float64 registers an float64 flag.
func (f *Flags) Float64(short, long string, defaultValue float64, help string) {
f.register(short, long, help, "float", true, defaultValue,
func(res FlagMap) {
res[long] = &FlagMapItem{
Value: defaultValue,
IsDefault: true,
}
},
func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) {
if !f.match(flag, short, long) {
return args, false, nil
}
var vStr string
if len(equalVal) > 0 {
vStr = equalVal
} else if len(args) > 0 {
vStr = args[0]
args = args[1:]
} else {
return args, false, fmt.Errorf("missing float value for flag: %s", flag)
}
i, err := strconv.ParseFloat(vStr, 64)
if err != nil {
return args, false, fmt.Errorf("invalid float value for flag: %s", flag)
}
res[long] = &FlagMapItem{
Value: i,
IsDefault: false,
}
return args, true, nil
})
}
// DurationL same as Duration, but without a shorthand.
func (f *Flags) DurationL(long string, defaultValue time.Duration, help string) {
f.Duration("", long, defaultValue, help)
}
// Duration registers a duration flag.
func (f *Flags) Duration(short, long string, defaultValue time.Duration, help string) {
f.register(short, long, help, "duration", true, defaultValue,
func(res FlagMap) {
res[long] = &FlagMapItem{
Value: defaultValue,
IsDefault: true,
}
},
func(flag, equalVal string, args []string, res FlagMap) ([]string, bool, error) {
if !f.match(flag, short, long) {
return args, false, nil
}
var vStr string
if len(equalVal) > 0 {
vStr = equalVal
} else if len(args) > 0 {
vStr = args[0]
args = args[1:]
} else {
return args, false, fmt.Errorf("missing duration value for flag: %s", flag)
}
d, err := time.ParseDuration(vStr)
if err != nil {
return args, false, fmt.Errorf("invalid duration value for flag: %s", flag)
}
res[long] = &FlagMapItem{
Value: d,
IsDefault: false,
}
return args, true, nil
})
}
func trimQuotes(s string) string {
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
return s[1 : len(s)-1]
}
return s
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package grumble
import (
"fmt"
"os"
"sort"
"github.com/desertbit/columnize"
)
func defaultInterruptHandler(a *App, count int) {
if count >= 2 {
a.Println("interrupted")
os.Exit(1)
}
a.Println("input Ctrl-c once more to exit")
}
func defaultPrintHelp(a *App, shell bool) {
// Columnize options.
config := columnize.DefaultConfig()
config.Delim = "|"
config.Glue = " "
config.Prefix = " "
// ASCII logo.
if a.printASCIILogo != nil {
a.printASCIILogo(a)
}
// Description.
if (len(a.config.Description)) > 0 {
a.Printf("\n%s\n", a.config.Description)
}
// Usage.
if !shell {
a.Println()
printHeadline(a, "Usage:")
a.Printf(" %s [command]\n", a.config.Name)
}
// Group the commands by their help group if present.
groups := make(map[string]*Commands)
for _, c := range a.commands.list {
key := c.HelpGroup
if len(key) == 0 {
key = "Commands:"
}
cc := groups[key]
if cc == nil {
cc = new(Commands)
groups[key] = cc
}
cc.Add(c)
}
// Sort the map by the keys.
var keys []string
for k := range groups {
keys = append(keys, k)
}
sort.Strings(keys)
// Print each commands group.
for _, headline := range keys {
cc := groups[headline]
cc.Sort()
var output []string
for _, c := range cc.list {
name := c.Name
for _, a := range c.Aliases {
name += ", " + a
}
output = append(output, fmt.Sprintf("%s | %v", name, c.Help))
}
if len(output) > 0 {
a.Println()
printHeadline(a, headline)
a.Printf("%s\n", columnize.Format(output, config))
}
}
// Sub Commands.
if a.config.HelpSubCommands {
// Check if there is at least one sub command.
hasSubCmds := false
for _, c := range a.commands.list {
if len(c.commands.list) > 0 {
hasSubCmds = true
break
}
}
if hasSubCmds {
// Headline.
a.Println()
printHeadline(a, "Sub Commands:")
hp := headlinePrinter(a)
// Only print the first level of sub commands.
for _, c := range a.commands.list {
if len(c.commands.list) == 0 {
continue
}
var output []string
for _, c := range c.commands.list {
name := c.Name
for _, a := range c.Aliases {
name += ", " + a
}
output = append(output, fmt.Sprintf("%s | %v", name, c.Help))
}
a.Println()
_, _ = hp(c.Name + ":")
a.Printf("%s\n", columnize.Format(output, config))
}
}
}
// Flags.
if !shell {
printFlags(a, &a.flags)
}
a.Println()
}
func defaultPrintCommandHelp(a *App, cmd *Command, shell bool) {
// Columnize options.
config := columnize.DefaultConfig()
config.Delim = "|"
config.Glue = " "
config.Prefix = " "
// Help description.
if len(cmd.LongHelp) > 0 {
a.Printf("\n%s\n", cmd.LongHelp)
} else {
a.Printf("\n%s\n", cmd.Help)
}
// Usage.
printUsage(a, cmd)
// Arguments.
printArgs(a, &cmd.args)
// Flags.
printFlags(a, &cmd.flags)
// Sub Commands.
if len(cmd.commands.list) > 0 {
// Only print the first level of sub commands.
var output []string
for _, c := range cmd.commands.list {
name := c.Name
for _, a := range c.Aliases {
name += ", " + a
}
output = append(output, fmt.Sprintf("%s | %v", name, c.Help))
}
a.Println()
printHeadline(a, "Sub Commands:")
a.Printf("%s\n", columnize.Format(output, config))
}
a.Println()
}
func headlinePrinter(a *App) func(v ...interface{}) (int, error) {
if a.config.NoColor || a.config.HelpHeadlineColor == nil {
return a.Println
}
return func(v ...interface{}) (int, error) {
return a.config.HelpHeadlineColor.Fprintln(a, v...)
}
}
func printHeadline(a *App, s string) {
hp := headlinePrinter(a)
if a.config.HelpHeadlineUnderline {
_, _ = hp(s)
u := ""
for i := 0; i < len(s); i++ {
u += "="
}
_, _ = hp(u)
} else {
_, _ = hp(s)
}
}
func printUsage(a *App, cmd *Command) {
a.Println()
printHeadline(a, "Usage:")
// Print either the user-provided usage message or compose
// one on our own from the flags and args.
if len(cmd.Usage) > 0 {
a.Printf(" %s\n", cmd.Usage)
return
}
// Layout: Cmd [Flags] Args
a.Printf(" %s", cmd.Name)
if !cmd.flags.empty() {
a.Printf(" [flags]")
}
if !cmd.args.empty() {
for _, arg := range cmd.args.list {
name := arg.Name
if arg.isList {
name += "..."
}
if arg.optional {
a.Printf(" [%s]", name)
} else {
a.Printf(" %s", name)
}
if arg.isList && (arg.listMin != -1 || arg.listMax != -1) {
a.Printf("{")
if arg.listMin != -1 {
a.Printf("%d", arg.listMin)
}
a.Printf(",")
if arg.listMax != -1 {
a.Printf("%d", arg.listMax)
}
a.Printf("}")
}
}
}
a.Println()
}
func printArgs(a *App, args *Args) {
// Columnize options.
config := columnize.DefaultConfig()
config.Delim = "|"
config.Glue = " "
config.Prefix = " "
var output []string
for _, a := range args.list {
defaultValue := ""
if a.Default != nil && len(fmt.Sprintf("%v", a.Default)) > 0 && a.optional {
defaultValue = fmt.Sprintf("(default: %v)", a.Default)
}
output = append(output, fmt.Sprintf("%s || %s |||| %s %s", a.Name, a.HelpArgs, a.Help, defaultValue))
}
if len(output) > 0 {
a.Println()
printHeadline(a, "Args:")
a.Printf("%s\n", columnize.Format(output, config))
}
}
func printFlags(a *App, flags *Flags) {
// Columnize options.
config := columnize.DefaultConfig()
config.Delim = "|"
config.Glue = " "
config.Prefix = " "
flags.sort()
var output []string
for _, f := range flags.list {
long := "--" + f.Long
short := ""
if len(f.Short) > 0 {
short = "-" + f.Short + ","
}
defaultValue := ""
if f.Default != nil && f.HelpShowDefault && len(fmt.Sprintf("%v", f.Default)) > 0 {
defaultValue = fmt.Sprintf("(default: %v)", f.Default)
}
output = append(output, fmt.Sprintf("%s | %s | %s |||| %s %s", short, long, f.HelpArgs, f.Help, defaultValue))
}
if len(output) > 0 {
a.Println()
printHeadline(a, "Flags:")
a.Printf("%s\n", columnize.Format(output, config))
}
}
module github.com/desertbit/grumble
go 1.12
require (
github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667 // indirect
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/desertbit/closer/v3 v3.1.2
github.com/desertbit/columnize v2.1.0+incompatible
github.com/desertbit/go-shlex v0.1.1
github.com/desertbit/readline v1.5.1
github.com/fatih/color v1.10.0
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c // indirect
github.com/kr/pty v1.1.8 // indirect
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e // indirect
gopkg.in/AlecAivazis/survey.v1 v1.8.5
)
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667 h1:l2RCK7mjLhjfZRIcCXTVHI34l67IRtKASBjusViLzQ0=
github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/desertbit/closer/v3 v3.0.1 h1:mZORDd9VbXIgH8OrUyKrS+0sdvNwpR47JB5cgnBTc0I=
github.com/desertbit/closer/v3 v3.0.1/go.mod h1:AAC4KRd8DC40nwvV967J/kDFhujMEiuwIKQfN0IDxXw=
github.com/desertbit/closer/v3 v3.1.2 h1:a6+2DmwIcNygW04XXWYq+Qp2X9uIk9QbZCP9//qEkb0=
github.com/desertbit/closer/v3 v3.1.2/go.mod h1:AAC4KRd8DC40nwvV967J/kDFhujMEiuwIKQfN0IDxXw=
github.com/desertbit/columnize v2.1.0+incompatible h1:h55rYmdrWoTj7w9aAnCkxzM3C2Eb8zuFa2W41t0o5j0=
github.com/desertbit/columnize v2.1.0+incompatible/go.mod h1:5kPrzQwKbQ8E5D28nvTVPqIBJyj+8jvJzwt6HXZvXgI=
github.com/desertbit/go-shlex v0.1.0 h1:HapoSxMl/xT59s1Z+RR6ScBuH2H8vr4HcgERVjBTJ8s=
github.com/desertbit/go-shlex v0.1.0/go.mod h1:Qbb+mJNud5AypgHZ81EL8syOGaWlwvAOTqS7XmWI4pQ=
github.com/desertbit/go-shlex v0.1.1 h1:c65HnbgX1QyC6kPL1dMzUpZ4puNUE6ai/eVucWNLNsk=
github.com/desertbit/go-shlex v0.1.1/go.mod h1:Qbb+mJNud5AypgHZ81EL8syOGaWlwvAOTqS7XmWI4pQ=
github.com/desertbit/readline v0.0.0-20171208011716-f6d7a1f6fbf3 h1:PLbrJOpAtk8ros5avAt8am0GJvPuD08FVLV+ffmG0Jo=
github.com/desertbit/readline v0.0.0-20171208011716-f6d7a1f6fbf3/go.mod h1:AayRnEOr0ttSh4kgOlHBBtJufZMMW/1BVCdV5oFs8t0=
github.com/desertbit/readline v1.5.1 h1:/wOIZkWYl1s+IvJm/5bOknfUgs6MhS9svRNZpFM53Os=
github.com/desertbit/readline v1.5.1/go.mod h1:pHQgTsCFs9Cpfh5mlSUFi9Xa5kkL4d8L1Jo4UVWzPw0=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c h1:kp3AxgXgDOmIJFR7bIwqFhwJ2qWar8tEQSE5XXhCfVk=
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b h1:Elez2XeF2p9uyVj0yEUDqQ56NFcDtcBNkYP7yv8YbUE=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 h1:bNEHhJCnrwMKNMmOx3yAynp5vs5/gRy+XWFtZFu7NBM=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/AlecAivazis/survey.v1 v1.8.5 h1:QoEEmn/d5BbuPIL2qvXwzJdttFFhRQFkaq+tEKb7SMI=
gopkg.in/AlecAivazis/survey.v1 v1.8.5/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Package grumble is a powerful modern CLI and SHELL.
package grumble
import (
"fmt"
"os"
)
// Main is a shorthand to run the app within the main function.
// This function will handle the error and exit the application on error.
func Main(a *App) {
err := a.Run()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"fmt"
"github.com/desertbit/grumble"
)
func init() {
adminCommand := &grumble.Command{
Name: "admin",
Help: "admin tools",
LongHelp: "super administration tools",
}
App.AddCommand(adminCommand)
adminCommand.AddCommand(&grumble.Command{
Name: "root",
Help: "root the machine",
Run: func(c *grumble.Context) error {
fmt.Println(c.Flags.String("directory"))
return fmt.Errorf("failed")
},
})
adminCommand.AddCommand(&grumble.Command{
Name: "kill",
Help: "kill the process",
Run: func(c *grumble.Context) error {
return fmt.Errorf("failed")
},
})
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"github.com/desertbit/grumble"
"github.com/fatih/color"
)
var App = grumble.New(&grumble.Config{
Name: "foo",
Description: "An awesome foo bar",
HistoryFile: "/tmp/foo.hist",
Prompt: "foo » ",
PromptColor: color.New(color.FgGreen, color.Bold),
HelpHeadlineColor: color.New(color.FgGreen),
HelpHeadlineUnderline: true,
HelpSubCommands: true,
Flags: func(f *grumble.Flags) {
f.String("d", "directory", "DEFAULT", "set an alternative root directory path")
f.Bool("v", "verbose", false, "enable verbose mode")
},
})
func init() {
App.SetPrintASCIILogo(func(a *grumble.App) {
a.Println(" _ _ ")
a.Println(" ___ ___ _ _ _____| |_| |___ ")
a.Println("| . | _| | | | . | | -_|")
a.Println("|_ |_| |___|_|_|_|___|_|___|")
a.Println("|___| ")
a.Println()
})
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"fmt"
"strings"
"time"
"github.com/desertbit/grumble"
)
func init() {
App.AddCommand(&grumble.Command{
Name: "args",
Help: "test args",
Args: func(a *grumble.Args) {
a.String("s", "test string")
a.Duration("d", "test duration", grumble.Default(time.Second))
a.Int("i", "test int", grumble.Default(5))
a.Int64("i64", "test int64", grumble.Default(int64(-88)))
a.Uint("u", "test uint", grumble.Default(uint(66)))
a.Uint64("u64", "test uint64", grumble.Default(uint64(8888)))
a.Float64("f64", "test float64", grumble.Default(float64(5.889)))
a.StringList("sl", "test string list", grumble.Default([]string{"first", "second", "third"}), grumble.Max(3))
},
Run: func(c *grumble.Context) error {
fmt.Println("s ", c.Args.String("s"))
fmt.Println("d ", c.Args.Duration("d"))
fmt.Println("i ", c.Args.Int("i"))
fmt.Println("i64", c.Args.Int64("i64"))
fmt.Println("u ", c.Args.Uint("u"))
fmt.Println("u64", c.Args.Uint64("u64"))
fmt.Println("f64", c.Args.Float64("f64"))
fmt.Println("sl ", strings.Join(c.Args.StringList("sl"), ","))
return nil
},
})
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"fmt"
"github.com/desertbit/grumble"
"gopkg.in/AlecAivazis/survey.v1"
)
func init() {
App.AddCommand(&grumble.Command{
Name: "ask",
Help: "ask the user for foo",
Run: func(c *grumble.Context) error {
ask()
return nil
},
})
}
// the questions to ask
var qs = []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{Message: "What is your name?"},
Validate: survey.Required,
Transform: survey.Title,
},
{
Name: "color",
Prompt: &survey.Select{
Message: "Choose a color:",
Options: []string{"red", "blue", "green"},
Default: "red",
},
},
{
Name: "age",
Prompt: &survey.Input{Message: "How old are you?"},
},
}
func ask() {
password := ""
prompt := &survey.Password{
Message: "Please type your password",
}
survey.AskOne(prompt, &password, nil)
// the answers will be written to this struct
answers := struct {
Name string // survey will match the question and field names
FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name
Age int // if the types don't match exactly, survey will try to convert for you
}{}
// perform the questions
err := survey.Ask(qs, &answers)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor)
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"strings"
"time"
"github.com/desertbit/grumble"
)
func init() {
App.AddCommand(&grumble.Command{
Name: "daemon",
Help: "run the daemon",
Aliases: []string{"run"},
Flags: func(f *grumble.Flags) {
f.Duration("t", "timeout", time.Second, "timeout duration")
},
Args: func(a *grumble.Args) {
a.Bool("production", "whether to start the daemon in production or development mode")
a.Int("opt-level", "the optimization mode", grumble.Default(3))
a.StringList("services", "additional services that should be started", grumble.Default([]string{"test", "te11"}))
},
Run: func(c *grumble.Context) error {
c.App.Println("timeout:", c.Flags.Duration("timeout"))
c.App.Println("directory:", c.Flags.String("directory"))
c.App.Println("verbose:", c.Flags.Bool("verbose"))
c.App.Println("production:", c.Args.Bool("production"))
c.App.Println("opt-level:", c.Args.Int("opt-level"))
c.App.Println("services:", strings.Join(c.Args.StringList("services"), ","))
return nil
},
})
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"fmt"
"time"
"github.com/desertbit/grumble"
)
func init() {
App.AddCommand(&grumble.Command{
Name: "flags",
Help: "test flags",
Flags: func(f *grumble.Flags) {
f.Duration("d", "duration", time.Second, "duration test")
f.Int("i", "int", 1, "test int")
f.Int64("l", "int64", 2, "test int64")
f.Uint("u", "uint", 3, "test uint")
f.Uint64("j", "uint64", 4, "test uint64")
f.Float64("f", "float", 5.55, "test float64")
},
Run: func(c *grumble.Context) error {
fmt.Println("duration ", c.Flags.Duration("duration"))
fmt.Println("int ", c.Flags.Int("int"))
fmt.Println("int64 ", c.Flags.Int64("int64"))
fmt.Println("uint ", c.Flags.Uint("uint"))
fmt.Println("uint64 ", c.Flags.Uint64("uint64"))
fmt.Println("float ", c.Flags.Float64("float"))
return nil
},
})
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"github.com/desertbit/grumble"
)
func init() {
promptCommand := &grumble.Command{
Name: "prompt",
Help: "set a custom prompt",
}
App.AddCommand(promptCommand)
promptCommand.AddCommand(&grumble.Command{
Name: "set",
Help: "set a custom prompt",
Run: func(c *grumble.Context) error {
c.App.SetPrompt("CUSTOM PROMPT >> ")
return nil
},
})
promptCommand.AddCommand(&grumble.Command{
Name: "reset",
Help: "reset to default prompt",
Run: func(c *grumble.Context) error {
c.App.SetDefaultPrompt()
return nil
},
})
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package main
import (
"github.com/desertbit/grumble"
"github.com/desertbit/grumble/sample/full/cmd"
)
func main() {
grumble.Main(cmd.App)
}
## Remote readline example
This is demonstration of remote shell access with grumble.
### How to run
First run the server in main directory:
```shell
./readline> go run .
```
Then establish a CLI connection with client app in **cli** subfolder
```shell
./readline> cd cli
./readline/cli> go run .
```
\ No newline at end of file
package main
import (
"fmt"
"github.com/desertbit/readline"
)
func main() {
if err := readline.DialRemote("tcp", ":5555"); err != nil {
fmt.Errorf("An error occurred: %s \n", err.Error())
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"errors"
"strings"
"time"
"github.com/desertbit/grumble"
)
var App = grumble.New(&grumble.Config{
Name: "foo",
Description: "An awesome foo bar",
Flags: func(f *grumble.Flags) {
f.String("d", "directory", "DEFAULT", "set an alternative root directory path")
f.Bool("v", "verbose", false, "enable verbose mode")
},
})
func init() {
App.AddCommand(&grumble.Command{
Name: "daemon",
Help: "run the daemon",
Aliases: []string{"run"},
Flags: func(f *grumble.Flags) {
f.Duration("t", "timeout", time.Second, "timeout duration")
},
Args: func(a *grumble.Args) {
a.Bool("production", "whether to start the daemon in production or development mode")
a.Int("opt-level", "the optimization mode", grumble.Default(3))
a.StringList("services", "additional services that should be started", grumble.Default([]string{"test", "te11"}))
},
Run: func(c *grumble.Context) error {
c.App.Println("timeout:", c.Flags.Duration("timeout"))
c.App.Println("directory:", c.Flags.String("directory"))
c.App.Println("verbose:", c.Flags.Bool("verbose"))
c.App.Println("production:", c.Args.Bool("production"))
c.App.Println("opt-level:", c.Args.Int("opt-level"))
c.App.Println("services:", strings.Join(c.Args.StringList("services"), ","))
return nil
},
})
adminCommand := &grumble.Command{
Name: "admin",
Help: "admin tools",
LongHelp: "super administration tools",
}
App.AddCommand(adminCommand)
adminCommand.AddCommand(&grumble.Command{
Name: "root",
Help: "root the machine",
Run: func(c *grumble.Context) error {
c.App.Println(c.Flags.String("directory"))
return errors.New("failed")
},
})
}
package main
import (
"github.com/desertbit/grumble"
"github.com/desertbit/readline"
"time"
)
func main() {
handleFunc := func(rl *readline.Instance) {
var app = grumble.New(&grumble.Config{
Name: "app",
Description: "short app description",
InterruptHandler: func(a *grumble.App, count int) {
// do nothing
},
Flags: func(f *grumble.Flags) {
f.String("d", "directory", "DEFAULT", "set an alternative directory path")
f.Bool("v", "verbose", false, "enable verbose mode")
},
})
app.AddCommand(&grumble.Command{
Name: "daemon",
Help: "run the daemon",
Aliases: []string{"run"},
Flags: func(f *grumble.Flags) {
f.Duration("t", "timeout", time.Second, "timeout duration")
},
Args: func(a *grumble.Args) {
a.String("service", "which service to start", grumble.Default("server"))
},
Run: func(c *grumble.Context) error {
// Parent Flags.
c.App.Println("directory:", c.Flags.String("directory"))
c.App.Println("verbose:", c.Flags.Bool("verbose"))
// Flags.
c.App.Println("timeout:", c.Flags.Duration("timeout"))
// Args.
c.App.Println("service:", c.Args.String("service"))
return nil
},
})
adminCommand := &grumble.Command{
Name: "admin",
Help: "admin tools",
LongHelp: "super administration tools",
}
app.AddCommand(adminCommand)
app.RunWithReadline(rl)
}
cfg := &readline.Config{}
readline.ListenRemote("tcp", ":5555", cfg, handleFunc)
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package cmd
import (
"errors"
"strings"
"time"
"github.com/desertbit/grumble"
)
var App = grumble.New(&grumble.Config{
Name: "foo",
Description: "An awesome foo bar",
Flags: func(f *grumble.Flags) {
f.String("d", "directory", "DEFAULT", "set an alternative root directory path")
f.Bool("v", "verbose", false, "enable verbose mode")
},
})
func init() {
App.AddCommand(&grumble.Command{
Name: "daemon",
Help: "run the daemon",
Aliases: []string{"run"},
Flags: func(f *grumble.Flags) {
f.Duration("t", "timeout", time.Second, "timeout duration")
},
Args: func(a *grumble.Args) {
a.Bool("production", "whether to start the daemon in production or development mode")
a.Int("opt-level", "the optimization mode", grumble.Default(3))
a.StringList("services", "additional services that should be started", grumble.Default([]string{"test", "te11"}))
},
Run: func(c *grumble.Context) error {
c.App.Println("timeout:", c.Flags.Duration("timeout"))
c.App.Println("directory:", c.Flags.String("directory"))
c.App.Println("verbose:", c.Flags.Bool("verbose"))
c.App.Println("production:", c.Args.Bool("production"))
c.App.Println("opt-level:", c.Args.Int("opt-level"))
c.App.Println("services:", strings.Join(c.Args.StringList("services"), ","))
return nil
},
})
adminCommand := &grumble.Command{
Name: "admin",
Help: "admin tools",
LongHelp: "super administration tools",
}
App.AddCommand(adminCommand)
adminCommand.AddCommand(&grumble.Command{
Name: "root",
Help: "root the machine",
Run: func(c *grumble.Context) error {
c.App.Println(c.Flags.String("directory"))
return errors.New("failed")
},
})
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 Roland Singer [roland.singer@deserbit.com]
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package main
import (
"github.com/desertbit/grumble"
"github.com/desertbit/grumble/sample/simple/cmd"
)
func main() {
grumble.Main(cmd.App)
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册