diff --git a/.example/os/gcmd/manager/main.go b/.example/os/gcmd/manager/main.go new file mode 100644 index 000000000..e7ed739e0 --- /dev/null +++ b/.example/os/gcmd/manager/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" +) + +func main() { + var err error + c := &gcmd.Command{ + Name: "gf", + Description: `GoFrame Command Line Interface, which is your helpmate for building GoFrame application with convenience.`, + Additional: ` +Use 'gf help COMMAND' or 'gf COMMAND -h' for detail about a command, which has '...' in the tail of their comments.`, + } + // env + commandEnv := gcmd.Command{ + Name: "env", + Brief: "show current Golang environment variables", + Description: "show current Golang environment variables", + Func: func(parser *gcmd.Parser) { + + }, + } + if err = c.AddCommand(commandEnv); err != nil { + g.Log().Fatal(err) + } + // get + commandGet := gcmd.Command{ + Name: "get", + Brief: "install or update GF to system in default...", + Description: "show current Golang environment variables", + + Examples: ` +gf get github.com/gogf/gf +gf get github.com/gogf/gf@latest +gf get github.com/gogf/gf@master +gf get golang.org/x/sys +`, + Func: func(parser *gcmd.Parser) { + + }, + } + if err = c.AddCommand(commandGet); err != nil { + g.Log().Fatal(err) + } + // build + //-n, --name output binary name + //-v, --version output binary version + //-a, --arch output binary architecture, multiple arch separated with ',' + //-s, --system output binary system, multiple os separated with ',' + //-o, --output output binary path, used when building single binary file + //-p, --path output binary directory path, default is './bin' + //-e, --extra extra custom "go build" options + //-m, --mod like "-mod" option of "go build", use "-m none" to disable go module + //-c, --cgo enable or disable cgo feature, it's disabled in default + + commandBuild := gcmd.Command{ + Name: "build", + Usage: "gf build FILE [OPTION]", + Brief: "cross-building go project for lots of platforms...", + Description: ` +The "build" command is most commonly used command, which is designed as a powerful wrapper for +"go build" command for convenience cross-compiling usage. +It provides much more features for building binary: +1. Cross-Compiling for many platforms and architectures. +2. Configuration file support for compiling. +3. Build-In Variables. +`, + Examples: ` +gf build main.go +gf build main.go --swagger +gf build main.go --pack public,template +gf build main.go --cgo +gf build main.go -m none +gf build main.go -n my-app -a all -s all +gf build main.go -n my-app -a amd64,386 -s linux -p . +gf build main.go -n my-app -v 1.0 -a amd64,386 -s linux,windows,darwin -p ./docker/bin +`, + Func: func(parser *gcmd.Parser) { + + }, + } + if err = c.AddCommand(commandBuild); err != nil { + g.Log().Fatal(err) + } + c.Run() +} diff --git a/internal/command/command.go b/internal/command/command.go index 824f83ca3..ab63ae506 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -20,7 +20,7 @@ var ( argumentRegex = regexp.MustCompile(`^\-{1,2}([\w\?\.\-]+)(=){0,1}(.*)$`) ) -// Custom initialization. +// Init does custom initialization. func Init(args ...string) { if len(args) == 0 { if len(defaultParsedArgs) == 0 && len(defaultParsedOptions) == 0 { @@ -33,30 +33,38 @@ func Init(args ...string) { defaultParsedOptions = make(map[string]string) } // Parsing os.Args with default algorithm. + defaultParsedArgs, defaultParsedOptions = ParseUsingDefaultAlgorithm(args...) +} + +// ParseUsingDefaultAlgorithm parses arguments using default algorithm. +func ParseUsingDefaultAlgorithm(args ...string) (parsedArgs []string, parsedOptions map[string]string) { + parsedArgs = make([]string, 0) + parsedOptions = make(map[string]string) for i := 0; i < len(args); { array := argumentRegex.FindStringSubmatch(args[i]) if len(array) > 2 { if array[2] == "=" { - defaultParsedOptions[array[1]] = array[3] + parsedOptions[array[1]] = array[3] } else if i < len(args)-1 { if len(args[i+1]) > 0 && args[i+1][0] == '-' { // Eg: gf gen -d -n 1 - defaultParsedOptions[array[1]] = array[3] + parsedOptions[array[1]] = array[3] } else { // Eg: gf gen -n 2 - defaultParsedOptions[array[1]] = args[i+1] + parsedOptions[array[1]] = args[i+1] i += 2 continue } } else { // Eg: gf gen -h - defaultParsedOptions[array[1]] = array[3] + parsedOptions[array[1]] = array[3] } } else { - defaultParsedArgs = append(defaultParsedArgs, args[i]) + parsedArgs = append(parsedArgs, args[i]) } i++ } + return } // GetOpt returns the option value named `name`. diff --git a/os/gcmd/gcmd.go b/os/gcmd/gcmd.go index d32ca03d6..515491e46 100644 --- a/os/gcmd/gcmd.go +++ b/os/gcmd/gcmd.go @@ -15,11 +15,17 @@ import ( "strings" ) +const ( + helpOptionName = "help" + helpOptionNameShort = "h" + maxLineChars = 100 +) + var ( defaultCommandFuncMap = make(map[string]func()) ) -// Custom initialization. +// Init does custom initialization. func Init(args ...string) { command.Init(args...) } diff --git a/os/gcmd/gcmd_command.go b/os/gcmd/gcmd_command.go new file mode 100644 index 000000000..698f780e9 --- /dev/null +++ b/os/gcmd/gcmd_command.go @@ -0,0 +1,202 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. +// + +package gcmd + +import ( + "bytes" + "fmt" + "github.com/gogf/gf/errors/gerror" + "github.com/gogf/gf/internal/command" + "github.com/gogf/gf/text/gstr" + "os" +) + +type Command struct { + parent *Command + commands []Command + options []Option + level int + Name string + Usage string + Short string + Brief string + Description string + Func func(parser *Parser) + HelpFunc func(parser *Parser) + Examples string + Additional string +} + +type Option struct { + Name string + Short string + Brief string + Description string + NeedValue bool +} + +func (c *Command) Print() { + prefix := gstr.Repeat(" ", 4) + buffer := bytes.NewBuffer(nil) + // Usage. + if c.Usage != "" || c.Name != "" { + buffer.WriteString("USAGE\n") + buffer.WriteString(prefix) + if c.Usage != "" { + buffer.WriteString(c.Usage) + } else { + var ( + p = c + name = c.Name + ) + for p.parent != nil { + name = p.parent.Name + " " + name + p = p.parent + } + buffer.WriteString(fmt.Sprintf(`%s ARGUMENT [OPTION]`, name)) + } + buffer.WriteString("\n\n") + } + // Command. + if len(c.commands) > 0 { + buffer.WriteString("COMMAND\n") + maxSpaceLength := 0 + for _, cmd := range c.commands { + nameStr := cmd.Name + "/" + cmd.Short + if len(nameStr) > maxSpaceLength { + maxSpaceLength = len(nameStr) + } + } + for _, cmd := range c.commands { + nameStr := cmd.Name + if cmd.Short != "" { + nameStr += "/" + cmd.Short + } + var ( + spaceLength = maxSpaceLength - len(nameStr) + lineStr = fmt.Sprintf( + "%s%s%s %s\n", + prefix, nameStr, gstr.Repeat(" ", spaceLength), cmd.Brief, + ) + ) + lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n") + buffer.WriteString(lineStr) + } + buffer.WriteString("\n") + } + + // Examples. + if c.Examples != "" { + buffer.WriteString("EXAMPLES\n") + lineStr := gstr.WordWrap(gstr.Trim(c.Examples), maxLineChars, "\n") + for _, line := range gstr.SplitAndTrim(lineStr, "\n") { + buffer.WriteString(prefix) + buffer.WriteString(line) + buffer.WriteString("\n") + } + buffer.WriteString("\n") + } + // Description. + if c.Description != "" { + buffer.WriteString("DESCRIPTION\n") + lineStr := gstr.WordWrap(gstr.Trim(c.Description), maxLineChars, "\n") + for _, line := range gstr.SplitAndTrim(lineStr, "\n") { + buffer.WriteString(prefix) + buffer.WriteString(line) + buffer.WriteString("\n") + } + } + buffer.WriteString("\n") + // Additional. + if c.Additional != "" { + lineStr := gstr.WordWrap(gstr.Trim(c.Additional), maxLineChars, "\n") + buffer.WriteString(lineStr) + } + buffer.WriteString("\n") + fmt.Println(buffer.String()) +} + +func (c *Command) AddCommand(command ...Command) error { + for _, cmd := range command { + cmd.Name = gstr.Trim(cmd.Name) + if cmd.Name == "" { + return gerror.New("command name should not be empty") + } + if cmd.Func == nil { + return gerror.New("command function should not be empty") + } + cmd.parent = c + cmd.level = c.level + 1 + c.commands = append(c.commands, cmd) + } + return nil +} + +func (c *Command) AddOption(option ...Option) error { + for _, opt := range option { + opt.Name = gstr.Trim(opt.Name) + if opt.Name == "" { + return gerror.New("option name should not be empty") + } + } + c.options = append(c.options, option...) + return nil +} + +func (c *Command) Run() { + // Find the matched command and run it. + argument := GetArg(c.level + 1) + if argument != "" { + if len(c.commands) > 0 { + for _, cmd := range c.commands { + if gstr.Equal(cmd.Name, argument) { + cmd.Run() + return + } + } + } + } + // Run current command function. + var ( + err error + parser *Parser + ) + if len(c.options) > 0 { + optionParsingMap := make(map[string]bool, 0) + // Add custom options to parser. + for _, option := range c.options { + optionParsingKey := option.Name + if option.Short != "" { + optionParsingKey += "," + option.Short + } + optionParsingMap[optionParsingKey] = option.NeedValue + } + // Add help option to parser. + optionParsingMap[helpOptionName+","+helpOptionNameShort] = false + parser, err = Parse(optionParsingMap) + } else { + parsedArgs, parsedOptions := command.ParseUsingDefaultAlgorithm(os.Args...) + parser = &Parser{ + strict: false, + parsedArgs: parsedArgs, + parsedOptions: parsedOptions, + } + } + if err != nil { + fmt.Println("Error:", err) + } + if parser.ContainsOpt(helpOptionName) || parser.ContainsOpt(helpOptionNameShort) { + if c.HelpFunc != nil { + c.HelpFunc(parser) + } else { + c.Print() + } + return + } + c.Func(parser) +} diff --git a/os/gcmd/gcmd_handler.go b/os/gcmd/gcmd_handler.go index 2bcb7a905..b50045def 100644 --- a/os/gcmd/gcmd_handler.go +++ b/os/gcmd/gcmd_handler.go @@ -21,7 +21,7 @@ func BindHandle(cmd string, f func()) error { return nil } -// BindHandle registers callback function with map . +// BindHandleMap registers callback function with map . func BindHandleMap(m map[string]func()) error { var err error for k, v := range m { diff --git a/os/gcmd/gcmd_parser_handler.go b/os/gcmd/gcmd_parser_handler.go index a2ab79eee..ef9904295 100644 --- a/os/gcmd/gcmd_parser_handler.go +++ b/os/gcmd/gcmd_parser_handler.go @@ -21,7 +21,7 @@ func (p *Parser) BindHandle(cmd string, f func()) error { return nil } -// BindHandle registers callback function with map . +// BindHandleMap registers callback function with map . func (p *Parser) BindHandleMap(m map[string]func()) error { var err error for k, v := range m {