├── go.mod ├── Makefile ├── LICENSE ├── guinea_test.go ├── .github └── workflows │ └── ci.yml ├── options.go ├── options_test.go ├── guinea.go ├── context.go ├── doc.go ├── command_test.go ├── README.md └── command.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/boreq/guinea 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | ci: dependencies test 4 | 5 | doc: 6 | @echo "http://localhost:6060/pkg/github.com/boreq/guinea/" 7 | godoc -http=:6060 8 | 9 | test: 10 | go test ./... 11 | 12 | test-verbose: 13 | go test -v ./... 14 | 15 | test-short: 16 | go test -short ./... 17 | 18 | dependencies: 19 | go get -t ./... 20 | 21 | .PHONY: all ci doc test test-verbose test-short dependencies 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 boreq 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /guinea_test.go: -------------------------------------------------------------------------------- 1 | package guinea 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFindCommand(t *testing.T) { 8 | var subSubCmdA = Command{} 9 | 10 | var subSubCmdB = Command{} 11 | 12 | var subCmdA = Command{ 13 | Subcommands: map[string]*Command{ 14 | "subA": &subSubCmdA, 15 | "subB": &subSubCmdB, 16 | }, 17 | } 18 | 19 | var subCmdB = Command{} 20 | 21 | var mainCmd = Command{ 22 | Subcommands: map[string]*Command{ 23 | "subA": &subCmdA, 24 | "subB": &subCmdB, 25 | }, 26 | } 27 | 28 | // mainCmd --- subA --- subA 29 | // |_ subB |_ subB 30 | 31 | cmd, cmdName, cmdArgs := FindCommand(&mainCmd, []string{"program"}) 32 | if cmd != &mainCmd { 33 | t.Fatal("Invalid cmd") 34 | } 35 | if cmdName != "program" { 36 | t.Fatal("Invalid cmdName") 37 | } 38 | if len(cmdArgs) != 0 { 39 | t.Fatal("Invalid cmdArgs") 40 | } 41 | 42 | cmd, cmdName, cmdArgs = FindCommand(&mainCmd, []string{"program", "subA", "subB", "param1", "param2"}) 43 | if cmd != &subSubCmdB { 44 | t.Fatal("Invalid cmd") 45 | } 46 | if cmdName != "program subA subB" { 47 | t.Fatal("Invalid cmdName") 48 | } 49 | if len(cmdArgs) != 2 { 50 | t.Fatal("Invalid cmdArgs") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | branches: 9 | - 'master' 10 | 11 | jobs: 12 | 13 | ci: 14 | name: Run CI 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | go-version: ['1.19', '1.20'] 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v3 25 | with: 26 | go-version: ${{ matrix.version }} 27 | 28 | - name: Determine Go cache paths 29 | id: golang-path 30 | run: | 31 | echo "build=$(go env GOCACHE)" >>"$GITHUB_OUTPUT" 32 | echo "module=$(go env GOMODCACHE)" >>"$GITHUB_OUTPUT" 33 | shell: bash 34 | 35 | - name: Setup Go cache 36 | uses: actions/cache@v3 37 | with: 38 | path: | 39 | ${{ steps.golang-path.outputs.build }} 40 | ${{ steps.golang-path.outputs.module }} 41 | key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum', 'Makefile') }} 42 | restore-keys: | 43 | ${{ runner.os }}-golang- 44 | 45 | - name: Run tests 46 | run: make ci 47 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package guinea 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ValType int 8 | 9 | const ( 10 | String ValType = iota 11 | Bool 12 | Int 13 | ) 14 | 15 | // Option represents an optional flag. 16 | type Option struct { 17 | Name string 18 | Type ValType 19 | Default interface{} 20 | Description string 21 | } 22 | 23 | // String prepends the option name with one or two leading dashes and returns 24 | // it. It is used to generate help texts. 25 | func (opt Option) String() string { 26 | prefix := "-" 27 | if len(opt.Name) > 1 { 28 | prefix = "--" 29 | } 30 | return prefix + opt.Name 31 | } 32 | 33 | // Argument represents a required argument. 34 | type Argument struct { 35 | Name string 36 | Multiple bool 37 | Optional bool 38 | Description string 39 | } 40 | 41 | // String places the argument name in angle brackets and appends three dots to 42 | // it in order to indicate multiple arguments. It is used to generate help 43 | // texts. 44 | func (arg Argument) String() string { 45 | format := "<%s>" 46 | if arg.Multiple { 47 | format = fmt.Sprintf("%s...", format) 48 | } 49 | if arg.Optional { 50 | format = fmt.Sprintf("[%s]", format) 51 | } 52 | return fmt.Sprintf(format, arg.Name) 53 | } 54 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package guinea 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestOptionShortName(t *testing.T) { 8 | opt := Option{Name: "o"} 9 | if opt.String() != "-o" { 10 | t.Fatal("Invalid:", opt.String()) 11 | } 12 | } 13 | 14 | func TestOptionLongName(t *testing.T) { 15 | opt := Option{Name: "option"} 16 | if opt.String() != "--option" { 17 | t.Fatal("Invalid:", opt.String()) 18 | } 19 | } 20 | 21 | func TestArgumentSingularName(t *testing.T) { 22 | arg := Argument{Name: "argument", Multiple: false} 23 | if arg.String() != "" { 24 | t.Fatal("Invalid:", arg.String()) 25 | } 26 | } 27 | 28 | func TestArgumentMultipleName(t *testing.T) { 29 | arg := Argument{Name: "argument", Multiple: true} 30 | if arg.String() != "..." { 31 | t.Fatal("Invalid:", arg.String()) 32 | } 33 | } 34 | 35 | func TestArgumentOptionalName(t *testing.T) { 36 | arg := Argument{Name: "argument", Multiple: false, Optional: true} 37 | if arg.String() != "[]" { 38 | t.Fatal("Invalid:", arg.String()) 39 | } 40 | } 41 | 42 | func TestArgumentMultipleOptionalName(t *testing.T) { 43 | arg := Argument{Name: "argument", Multiple: true, Optional: true} 44 | if arg.String() != "[...]" { 45 | t.Fatal("Invalid:", arg.String()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /guinea.go: -------------------------------------------------------------------------------- 1 | package guinea 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | var globalOpt = []Option{ 9 | Option{ 10 | Name: "help", 11 | Type: Bool, 12 | Default: false, 13 | Description: "Display help", 14 | }, 15 | } 16 | 17 | // Run is a high level function which adds special behaviour to the commands, 18 | // namely displaying help to the user. If you wish to use the library without 19 | // that feature use the FindCommand function directly. 20 | func Run(rootCommand *Command) error { 21 | cmd, cmdName, cmdArgs := FindCommand(rootCommand, os.Args) 22 | cmd.Options = append(cmd.Options, globalOpt...) 23 | return cmd.Execute(cmdName, cmdArgs) 24 | } 25 | 26 | // FindCommand attempts to recursively locate the command which should be 27 | // executed. The provided command should be the root command of the program 28 | // containing all other subcommands. The array containing the provided 29 | // arguments will most likely be the os.Args array. The function returns the 30 | // located subcommand, its name and the remaining unused arguments. Those 31 | // values should be passed to the Command.Execute method. 32 | func FindCommand(cmd *Command, args []string) (*Command, string, []string) { 33 | foundCmd, foundArgs := findCommand(cmd, args[1:]) 34 | foundName := subcommandName(args, foundArgs) 35 | return foundCmd, foundName, foundArgs 36 | } 37 | 38 | func findCommand(cmd *Command, args []string) (*Command, []string) { 39 | for subCmdName, subCmd := range cmd.Subcommands { 40 | if len(args) > 0 && args[0] == subCmdName { 41 | return findCommand(subCmd, args[1:]) 42 | } 43 | } 44 | return cmd, args 45 | } 46 | 47 | func subcommandName(originalArgs []string, remainingArgs []string) string { 48 | argOffset := len(originalArgs) - len(remainingArgs) 49 | return strings.Join(originalArgs[:argOffset], " ") 50 | } 51 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package guinea 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | // OptionValue stores the value of a parsed option as returned by the standard 8 | // library flag package. The helper methods can be used to cast the value 9 | // quickly but they will only succeed if the defined type of the option matches 10 | // the called method. 11 | type OptionValue struct { 12 | Value interface{} 13 | } 14 | 15 | // Bool casts a value to a bool and panics on failure. 16 | func (v OptionValue) Bool() bool { 17 | return *v.Value.(*bool) 18 | } 19 | 20 | // Int casts a value to an int and panics on failure. 21 | func (v OptionValue) Int() int { 22 | return *v.Value.(*int) 23 | } 24 | 25 | // Str casts a value to a string and panics on failure. 26 | func (v OptionValue) Str() string { 27 | return *v.Value.(*string) 28 | } 29 | 30 | // Context holds the options and arguments provided by the user. 31 | type Context struct { 32 | Options map[string]OptionValue 33 | Arguments []string 34 | } 35 | 36 | func makeContext(c Command, args []string) (*Context, error) { 37 | context := &Context{ 38 | Options: make(map[string]OptionValue), 39 | } 40 | 41 | flagset := flag.NewFlagSet("sth", flag.ContinueOnError) 42 | flagset.Usage = func() {} 43 | for _, option := range c.Options { 44 | switch option.Type { 45 | case String: 46 | if option.Default == nil { 47 | option.Default = "" 48 | } 49 | context.Options[option.Name] = OptionValue{ 50 | Value: flagset.String(option.Name, option.Default.(string), ""), 51 | } 52 | case Bool: 53 | if option.Default == nil { 54 | option.Default = false 55 | } 56 | context.Options[option.Name] = OptionValue{ 57 | Value: flagset.Bool(option.Name, option.Default.(bool), ""), 58 | } 59 | case Int: 60 | if option.Default == nil { 61 | option.Default = 0 62 | } 63 | context.Options[option.Name] = OptionValue{ 64 | Value: flagset.Int(option.Name, option.Default.(int), ""), 65 | } 66 | } 67 | } 68 | if err := flagset.Parse(args); err != nil { 69 | return nil, err 70 | } 71 | context.Arguments = flagset.Args() 72 | return context, nil 73 | } 74 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package guinea is a command line interface library. 3 | 4 | Defining commands 5 | 6 | This library operates on a tree-like structure of available commands. In the 7 | following example we define a root command with two subcommands. It will most 8 | likely be the best to define the commands as global variables in your package. 9 | 10 | var rootCommand = guinea.Command{ 11 | Run: func(c guinea.Context) error { 12 | fmt.Println("This is a root command.") 13 | return nil 14 | }, 15 | Subcommands: map[string]*guinea.Command{ 16 | "subcommandA": &subCommandA, 17 | "subcommandB": &subCommandB, 18 | }, 19 | } 20 | 21 | var subCommandA = guinea.Command{ 22 | Run: func(c guinea.Context) error { 23 | fmt.Println("This is the first subcommand.") 24 | return nil 25 | }, 26 | } 27 | 28 | var subCommandB = guinea.Command{ 29 | Run: func(c guinea.Context) error { 30 | fmt.Println("This is the second subcommand.") 31 | return nil 32 | }, 33 | } 34 | 35 | Executing the commands 36 | 37 | After defining the commands use the run function to execute them. The library 38 | will read os.Args to determine which command should be executed and to populate 39 | the context passed to it with options and arguments. 40 | 41 | if err := guinea.Run(&rootCommand); err != nil { 42 | fmt.Fprintln(os.Stderr, err) 43 | } 44 | 45 | Using the program 46 | 47 | The user can invoke a program in multiple ways. 48 | 49 | $ ./example_program 50 | $ ./example_program subcommandA 51 | $ ./example_program subcommandB 52 | 53 | Passing options and arguments 54 | 55 | To let the user call a command with arguments or options populate the proper 56 | lists in the command struct. 57 | 58 | var parametrizedCommand = guinea.Command{ 59 | Run: func(c guinea.Context) error { 60 | fmt.Printf("Argument: %s\n", c.Arguments[0]) 61 | fmt.Printf("Option: %d\n", c.Options["myopt"].Int()) 62 | return nil 63 | }, 64 | Arguments: []guinea.Argument{ 65 | guinea.Argument{ 66 | Name: "myargument", 67 | Description: "An argument of a command.", 68 | }, 69 | }, 70 | Options: []guinea.Option{ 71 | guinea.Option{ 72 | Name: "myopt", 73 | Type: guinea.Int, 74 | Description: "An option which accepts an integer.", 75 | Default: 1, 76 | }, 77 | }, 78 | } 79 | 80 | If you wish to parse the arguments in a different way simply don't define any 81 | options or arguments in the command struct and pass the arguments from the 82 | context to your parsing function. 83 | 84 | */ 85 | package guinea 86 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package guinea 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | // Assigns /dev/null to stdout and returns a function for restoring it to the original one. 9 | func supressStdout() func() { 10 | stdout := os.Stdout 11 | os.Stdout, _ = os.Open(os.DevNull) 12 | return func() { 13 | os.Stdout = stdout 14 | } 15 | } 16 | 17 | func TestCommandTooFewArguments(t *testing.T) { 18 | var mainCmd = Command{ 19 | Arguments: []Argument{ 20 | {Name: "arg1"}, 21 | {Name: "arg2"}, 22 | }, 23 | } 24 | 25 | restoreStdout := supressStdout() 26 | defer restoreStdout() 27 | 28 | if mainCmd.Execute("program", []string{"a"}) != ErrInvalidParms { 29 | t.Fatal("Execute did not return ErrInvalidParams") 30 | } 31 | } 32 | 33 | func TestCommandTooManyArguments(t *testing.T) { 34 | var mainCmd = Command{ 35 | Arguments: []Argument{ 36 | {Name: "arg1"}, 37 | {Name: "arg2"}, 38 | }, 39 | } 40 | 41 | restoreStdout := supressStdout() 42 | defer restoreStdout() 43 | 44 | if mainCmd.Execute("program", []string{"a", "b", "c"}) != ErrInvalidParms { 45 | t.Fatal("Execute did not return ErrInvalidParams") 46 | } 47 | } 48 | 49 | func TestCommandOptionalArguments(t *testing.T) { 50 | var mainCmd = Command{ 51 | Arguments: []Argument{ 52 | {Name: "arg1"}, 53 | {Name: "arg2", Optional: true}, 54 | }, 55 | } 56 | 57 | restoreStdout := supressStdout() 58 | defer restoreStdout() 59 | 60 | if err := mainCmd.Execute("program", []string{"a"}); err != nil { 61 | t.Fatalf("Execute did returned %s", err) 62 | } 63 | } 64 | 65 | func TestCommandMultipleArguments(t *testing.T) { 66 | var mainCmd = Command{ 67 | Arguments: []Argument{ 68 | {Name: "arg1"}, 69 | {Name: "arg2", Multiple: true}, 70 | }, 71 | } 72 | 73 | restoreStdout := supressStdout() 74 | defer restoreStdout() 75 | 76 | if err := mainCmd.Execute("program", []string{"a", "b", "c"}); err != nil { 77 | t.Fatalf("Execute did returned %s", err) 78 | } 79 | } 80 | 81 | func TestExecuteCommandWithArgsPassingHelpShouldntReturnError(t *testing.T) { 82 | var cmd = Command{ 83 | Options: []Option{ 84 | { 85 | Name: "help", 86 | Type: Bool, 87 | Default: false, 88 | Description: "Display help", 89 | }, 90 | }, 91 | Arguments: []Argument{ 92 | {Name: "arg"}, 93 | }, 94 | } 95 | 96 | restoreStdout := supressStdout() 97 | defer restoreStdout() 98 | 99 | if err := cmd.Execute("prog sub", []string{"--help"}); err != nil { 100 | t.Fatalf("Expected nil, got %v", err) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # guinea [![CI](https://github.com/boreq/guinea/actions/workflows/ci.yml/badge.svg)](https://github.com/boreq/guinea/actions/workflows/ci.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/boreq/guinea.svg)](https://pkg.go.dev/github.com/boreq/guinea) 2 | 3 | Guinea is a command line interface library. 4 | 5 | ## Description 6 | Programs very often organise the user interface in the form of subcommands. As 7 | an example the `go` command lets the user invoke multiple subcommands such as 8 | `go build` or `go get`. This library lets you nest any numbers of subcommands 9 | (which can be thought of as separate programs) in each other easily building 10 | complex user interfaces. 11 | 12 | 13 | ## Example 14 | This program implements a root command which displays the program version and 15 | two subcommands. 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/boreq/guinea" 22 | "os" 23 | ) 24 | 25 | var rootCommand = guinea.Command{ 26 | Options: []guinea.Option{ 27 | guinea.Option{ 28 | Name: "version", 29 | Type: guinea.Bool, 30 | Description: "Display version", 31 | }, 32 | }, 33 | Run: func(c guinea.Context) error { 34 | if c.Options["version"].Bool() { 35 | fmt.Println("v0.0.0-dev") 36 | return nil 37 | } 38 | return guinea.ErrInvalidParms 39 | }, 40 | Subcommands: map[string]*guinea.Command{ 41 | "display_text": &commandDisplayText, 42 | "greet": &commandGreet, 43 | }, 44 | ShortDescription: "an example program using the guinea library", 45 | Description: `This program demonstrates the use of a CLI library.`, 46 | } 47 | 48 | var commandDisplayText = guinea.Command{ 49 | Run: func(c guinea.Context) error { 50 | fmt.Println("Hello world!") 51 | return nil 52 | }, 53 | ShortDescription: "displays text on the screen", 54 | Description: `This is a subcommand that displays "Hello world!" on the screen.`, 55 | } 56 | 57 | var commandGreet = guinea.Command{ 58 | Arguments: []guinea.Argument{ 59 | guinea.Argument{ 60 | Name: "person", 61 | Multiple: false, 62 | Description: "a person to greet", 63 | }, 64 | }, 65 | Options: []guinea.Option{ 66 | guinea.Option{ 67 | Name: "times", 68 | Type: guinea.Int, 69 | Description: "Number of greetings", 70 | Default: 1, 71 | }, 72 | }, 73 | Run: func(c guinea.Context) error { 74 | for i := 0; i < c.Options["times"].Int(); i++ { 75 | fmt.Printf("Hello %s!\n", c.Arguments[0]) 76 | } 77 | return nil 78 | }, 79 | ShortDescription: "greets the specified person", 80 | Description: `This is a subcommand that greets the specified person.`, 81 | } 82 | 83 | func main() { 84 | if err := guinea.Run(&rootCommand); err != nil { 85 | fmt.Fprintln(os.Stderr, err) 86 | os.Exit(1) 87 | } 88 | } 89 | 90 | And here are the example invocations of the program: 91 | 92 | $ ./main --help 93 | $ ./main --version 94 | $ ./main display_text 95 | $ ./main hello --help 96 | $ ./main hello boreq 97 | $ ./main hello --times 10 boreq 98 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package guinea 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // ErrInvalidParms can be returned by a CommandFunction to automatically 10 | // display help text. 11 | var ErrInvalidParms = errors.New("invalid parameters") 12 | 13 | type CommandFunction func(Context) error 14 | 15 | // Command represents a single command which can be executed by the program. 16 | type Command struct { 17 | Run CommandFunction 18 | Subcommands map[string]*Command 19 | Options []Option 20 | Arguments []Argument 21 | ShortDescription string 22 | Description string 23 | } 24 | 25 | // PrintHelp prints the return value of Help to the standard output. 26 | func (c Command) PrintHelp(cmdName string) { 27 | fmt.Printf(c.Help(cmdName)) 28 | } 29 | 30 | // UsageString returns a short string containing the syntax of this command. 31 | // Command name should be set to one of the return values of FindCommand. 32 | func (c Command) UsageString(cmdName string) string { 33 | rw := cmdName 34 | if len(c.Subcommands) > 0 { 35 | rw += " " 36 | } 37 | rw += " []" 38 | for _, arg := range c.Arguments { 39 | rw += fmt.Sprintf(" %s", arg) 40 | } 41 | return rw 42 | } 43 | 44 | // Help returns the full help text for this command The text contains the 45 | // syntax of the command, a description, a list of accepted options and 46 | // arguments and available subcommands. Command name should be set to one of 47 | // the return values of FindCommand. 48 | func (c Command) Help(cmdName string) string { 49 | var rv string 50 | 51 | usage := c.UsageString(cmdName) 52 | rv += fmt.Sprintf("\n %s - %s\n", usage, c.ShortDescription) 53 | 54 | if len(c.Options) > 0 { 55 | rv += fmt.Sprintln("\nOPTIONS:") 56 | for _, opt := range c.Options { 57 | rv += fmt.Sprintf(" %-20s %s\n", opt, opt.Description) 58 | } 59 | } 60 | 61 | if len(c.Arguments) > 0 { 62 | rv += fmt.Sprintln("\nARGUMENTS:") 63 | for _, arg := range c.Arguments { 64 | rv += fmt.Sprintf(" %-20s %s\n", arg, arg.Description) 65 | } 66 | } 67 | 68 | if len(c.Subcommands) > 0 { 69 | rv += fmt.Sprintln("\nSUBCOMMANDS:") 70 | for name, subCmd := range c.Subcommands { 71 | rv += fmt.Sprintf(" %-20s %s\n", name, subCmd.ShortDescription) 72 | } 73 | rv += fmt.Sprintf("\n Try '%s --help'\n", cmdName) 74 | } 75 | 76 | if len(c.Description) > 0 { 77 | rv += fmt.Sprintln("\nDESCRIPTION:") 78 | desc := strings.Trim(c.Description, "\n") 79 | for _, line := range strings.Split(desc, "\n") { 80 | rv += fmt.Sprintf(" %s\n", line) 81 | } 82 | } 83 | 84 | return rv 85 | } 86 | 87 | // Execute runs the command. Command name is used to generate the help texts 88 | // and should usually be set to one of the return values of FindCommand. The 89 | // array of the arguments provided for this subcommand is used to generate the 90 | // context and should be set to one of the return values of FindCommand as 91 | // well. The command will not be executed with an insufficient number of 92 | // arguments so there is no need to check that in the run function. 93 | func (c Command) Execute(cmdName string, cmdArgs []string) error { 94 | context, err := makeContext(c, cmdArgs) 95 | if err != nil { 96 | c.PrintHelp(cmdName) 97 | return err 98 | } 99 | 100 | // Is there a help flag and is it set? 101 | if help, ok := context.Options["help"]; ok && help.Bool() { 102 | c.PrintHelp(cmdName) 103 | return nil 104 | } 105 | 106 | // Is the number of arguments sufficient? 107 | if err := c.validateArgs(context.Arguments); err != nil { 108 | c.PrintHelp(cmdName) 109 | return err 110 | } 111 | 112 | // Is this command only used to hold subcommands? 113 | if c.Run == nil { 114 | c.PrintHelp(cmdName) 115 | return nil 116 | } 117 | 118 | e := c.Run(*context) 119 | if e == ErrInvalidParms { 120 | c.PrintHelp(cmdName) 121 | } 122 | return e 123 | } 124 | 125 | func (c Command) validateArgs(cmdArgs []string) error { 126 | if len(cmdArgs) < c.minNumberOfArguments() { 127 | return ErrInvalidParms 128 | } 129 | max, err := c.maxNumberOfArguments() 130 | if err == nil && len(cmdArgs) > max { 131 | return ErrInvalidParms 132 | 133 | } 134 | return nil 135 | } 136 | 137 | // minNumberOfArguments returns the min number of arguments that can be passed 138 | // to this command. This is equal to the number of required arguments. 139 | func (c Command) minNumberOfArguments() int { 140 | sum := 0 141 | for _, argument := range c.Arguments { 142 | if !argument.Optional { 143 | sum++ 144 | } 145 | } 146 | return sum 147 | } 148 | 149 | // maxNumberOfArguments returns the max number of arguments that can be passed 150 | // to this command or error to indicate that the number is unlimited (if there 151 | // are arguments marked as "multiple" defined for this method). 152 | func (c Command) maxNumberOfArguments() (int, error) { 153 | for _, argument := range c.Arguments { 154 | if argument.Multiple { 155 | return 0, errors.New("unlimited number of arguments") 156 | } 157 | } 158 | return len(c.Arguments), nil 159 | } 160 | --------------------------------------------------------------------------------