improve command feature for package gcmd

This commit is contained in:
John Guo
2021-11-21 13:09:45 +08:00
parent dbee5810dc
commit e936b376fb
8 changed files with 430 additions and 322 deletions

View File

@ -1,88 +0,0 @@
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()
}

View File

@ -19,11 +19,7 @@ import (
const (
helpOptionName = "help"
helpOptionNameShort = "h"
maxLineChars = 100
)
var (
defaultCommandFuncMap = make(map[string]func())
maxLineChars = 120
)
// Init does custom initialization.

View File

@ -8,122 +8,51 @@
package gcmd
import (
"bytes"
"fmt"
"os"
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/command"
"github.com/gogf/gf/v2/text/gstr"
)
// Command holds the info about an argument that can handle custom logic.
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
Name string // Command name(case-sensitive).
Usage string // A brief line description about its usage, eg: gf build main.go [OPTION]
Brief string // A brief info that describes what this command will do.
Description string // A detailed description.
Options []Option // Option array, configuring how this command act.
Func Function // Custom function.
HelpFunc Function // Custom help function
Examples string // Usage examples.
Additional string // Additional custom info about this command.
parent *Command // Parent command for internal usage.
commands []Command // Sub commands of this command.
}
// Function is a custom command callback function that is bound to a certain argument.
type Function func(ctx context.Context, parser *Parser) (err error)
// Option is the command value that is specified by a name or shor name.
// An Option can have or have no value bound to it.
type Option struct {
Name string
Short string
Brief string
Description string
NeedValue bool
Name string // Option name.
Short string // Option short.
Brief string // Brief info about this Option, which is used in help info.
NeedValue bool // Whether this Option having or having no value bound to it.
}
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")
var (
// defaultHelpOption is the default help option that will be automatically added to each command.
defaultHelpOption = Option{
Name: `help`,
Short: `h`,
Brief: `more information about this command`,
NeedValue: false,
}
)
// 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 {
func (c *Command) Add(commands ...Command) error {
for _, cmd := range commands {
cmd.Name = gstr.Trim(cmd.Name)
if cmd.Name == "" {
return gerror.New("command name should not be empty")
@ -132,72 +61,7 @@ func (c *Command) AddCommand(command ...Command) error {
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.IsEmpty() {
if len(c.commands) > 0 {
for _, cmd := range c.commands {
if gstr.Equal(cmd.Name, argument.String()) {
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)
}

View File

@ -0,0 +1,130 @@
// 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"
"context"
"fmt"
"github.com/gogf/gf/v2/text/gstr"
)
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")
var (
maxSpaceLength = 0
)
for _, cmd := range c.commands {
if len(cmd.Name) > maxSpaceLength {
maxSpaceLength = len(cmd.Name)
}
}
for _, cmd := range c.commands {
// Add "..." to brief for those commands that also have sub-commands.
if len(cmd.commands) > 0 {
cmd.Brief = gstr.TrimRight(cmd.Brief, ".") + "..."
}
var (
spaceLength = maxSpaceLength - len(cmd.Name)
lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, cmd.Name, gstr.Repeat(" ", spaceLength+4), cmd.Brief)
wordwrapPrefix = gstr.Repeat(" ", len(prefix+cmd.Name)+spaceLength+4)
)
lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n"+wordwrapPrefix)
buffer.WriteString(lineStr)
}
buffer.WriteString("\n")
}
// Option.
if len(c.Options) > 0 {
buffer.WriteString("OPTION\n")
var (
nameStr string
maxSpaceLength = 0
)
for _, option := range c.Options {
if option.Short != "" {
nameStr = fmt.Sprintf("-%s,\t--%s", option.Short, option.Name)
} else {
nameStr = fmt.Sprintf("-/--%s", option.Name)
}
if len(nameStr) > maxSpaceLength {
maxSpaceLength = len(nameStr)
}
}
for _, option := range c.Options {
if option.Short != "" {
nameStr = fmt.Sprintf("-%s,\t--%s", option.Short, option.Name)
} else {
nameStr = fmt.Sprintf("-/--%s", option.Name)
}
var (
spaceLength = maxSpaceLength - len(nameStr)
lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, nameStr, gstr.Repeat(" ", spaceLength+4), option.Brief)
wordwrapPrefix = gstr.Repeat(" ", len(prefix+nameStr)+spaceLength+4)
)
lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n"+wordwrapPrefix)
buffer.WriteString(lineStr)
}
buffer.WriteString("\n")
}
// Example.
if c.Examples != "" {
buffer.WriteString("EXAMPLE\n")
buffer.WriteString(prefix)
buffer.WriteString(gstr.WordWrap(gstr.Trim(c.Examples), maxLineChars, "\n"+prefix))
buffer.WriteString("\n")
}
// Description.
if c.Description != "" {
buffer.WriteString("DESCRIPTION\n")
buffer.WriteString(prefix)
buffer.WriteString(gstr.WordWrap(gstr.Trim(c.Description), maxLineChars, "\n"+prefix))
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) defaultHelpFunc(ctx context.Context, parser *Parser) error {
c.Print()
return nil
}

View File

@ -0,0 +1,99 @@
// 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 (
"context"
"fmt"
"github.com/gogf/gf/v2/text/gstr"
)
func (c *Command) Run(ctx context.Context) error {
// Parse command arguments and options using default algorithm.
parser, err := Parse(nil)
if err != nil {
return err
}
args := parser.GetArgAll()
if len(args) == 1 {
if c.HelpFunc != nil {
return c.HelpFunc(ctx, parser)
}
return c.defaultHelpFunc(ctx, parser)
}
// Exclude the root binary name.
args = args[1:]
// Find the matched command and run it.
if subCommand := c.searchCommand(args); subCommand != nil {
return subCommand.doRun(ctx, parser)
}
// Print error and help command if no command found.
fmt.Printf("Error: command not found for \"%s\"\n\n", gstr.Join(args, " "))
c.Print()
return nil
}
func (c *Command) doRun(ctx context.Context, parser *Parser) (err error) {
// Add built-in help option, just for info only.
c.Options = append(c.Options, defaultHelpOption)
// Check built-in help command.
if parser.ContainsOpt(helpOptionName) || parser.ContainsOpt(helpOptionNameShort) {
if c.HelpFunc != nil {
return c.HelpFunc(ctx, parser)
}
return c.defaultHelpFunc(ctx, parser)
}
// Reparse the arguments for current command configuration.
parser, err = c.reParse(ctx, parser)
if err != nil {
return err
}
// Registered command function calling.
return c.Func(ctx, parser)
}
func (c *Command) reParse(ctx context.Context, parser *Parser) (*Parser, error) {
// It seems just has built-in help option, it so does nothing.
if len(c.Options) == 1 {
return parser, nil
}
var (
optionKey string
supportedOptions = make(map[string]bool)
)
for _, option := range c.Options {
if option.Short != "" {
optionKey = fmt.Sprintf(`%s.%s`, option.Name, option.Short)
} else {
optionKey = option.Name
}
supportedOptions[optionKey] = option.NeedValue
}
return Parse(supportedOptions)
}
func (c *Command) searchCommand(args []string) *Command {
if len(args) == 0 {
return nil
}
for _, cmd := range c.commands {
if cmd.Name == args[0] {
leftArgs := args[1:]
if len(leftArgs) == 0 {
return &cmd
}
return cmd.searchCommand(leftArgs)
}
}
return nil
}

View File

@ -1,59 +0,0 @@
// 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 (
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
)
// BindHandle registers callback function `f` with `cmd`.
func BindHandle(cmd string, f func()) error {
if _, ok := defaultCommandFuncMap[cmd]; ok {
return gerror.NewCode(gcode.CodeInvalidOperation, "duplicated handle for command:"+cmd)
} else {
defaultCommandFuncMap[cmd] = f
}
return nil
}
// BindHandleMap registers callback function with map `m`.
func BindHandleMap(m map[string]func()) error {
var err error
for k, v := range m {
if err = BindHandle(k, v); err != nil {
return err
}
}
return err
}
// RunHandle executes the callback function registered by `cmd`.
func RunHandle(cmd string) error {
if handle, ok := defaultCommandFuncMap[cmd]; ok {
handle()
} else {
return gerror.NewCode(gcode.CodeMissingConfiguration, "no handle found for command:"+cmd)
}
return nil
}
// AutoRun automatically recognizes and executes the callback function
// by value of index 0 (the first console parameter).
func AutoRun() error {
if cmd := GetArg(1); !cmd.IsEmpty() {
if handle, ok := defaultCommandFuncMap[cmd.String()]; ok {
handle()
} else {
return gerror.NewCode(gcode.CodeMissingConfiguration, "no handle found for command:"+cmd.String())
}
} else {
return gerror.NewCode(gcode.CodeMissingParameter, "no command found")
}
return nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/command"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
@ -25,7 +26,7 @@ type Parser struct {
parsedArgs []string // As name described.
parsedOptions map[string]string // As name described.
passedOptions map[string]bool // User passed supported options.
supportedOptions map[string]bool // Option [option name : need argument].
supportedOptions map[string]bool // Option [OptionName:WhetherNeedArgument].
commandFuncMap map[string]func() // Command function map for function handler.
}
@ -36,6 +37,13 @@ type Parser struct {
//
// The optional parameter `strict` specifies whether stops parsing and returns error if invalid option passed.
func Parse(supportedOptions map[string]bool, strict ...bool) (*Parser, error) {
if supportedOptions == nil {
command.Init(os.Args...)
return &Parser{
parsedArgs: GetArgAll(),
parsedOptions: GetOptAll(),
}, nil
}
return ParseWithArgs(os.Args, supportedOptions, strict...)
}
@ -46,6 +54,13 @@ func Parse(supportedOptions map[string]bool, strict ...bool) (*Parser, error) {
//
// The optional parameter `strict` specifies whether stops parsing and returns error if invalid option passed.
func ParseWithArgs(args []string, supportedOptions map[string]bool, strict ...bool) (*Parser, error) {
if supportedOptions == nil {
command.Init(args...)
return &Parser{
parsedArgs: GetArgAll(),
parsedOptions: GetOptAll(),
}, nil
}
strictParsing := false
if len(strict) > 0 {
strictParsing = strict[0]
@ -86,7 +101,7 @@ func ParseWithArgs(args []string, supportedOptions map[string]bool, strict ...bo
}
} else {
// Multiple options?
if array := parser.parseMultiOption(option); len(array) > 0 {
if array = parser.parseMultiOption(option); len(array) > 0 {
for _, v := range array {
parser.setOptionValue(v, "")
}

View File

@ -9,10 +9,13 @@
package gcmd_test
import (
"context"
"fmt"
"testing"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/genv"
"github.com/gogf/gf/v2/test/gtest"
)
@ -67,3 +70,151 @@ func Test_GetWithEnv(t *testing.T) {
t.Assert(gcmd.GetOptWithEnv("test"), 2)
})
}
func Test_Command(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
ctx = gctx.New()
err error
)
commandRoot := &gcmd.Command{
Name: "gf",
}
// env
commandEnv := gcmd.Command{
Name: "env",
Func: func(ctx context.Context, parser *gcmd.Parser) error {
fmt.Println("env")
return nil
},
}
// test
commandTest := gcmd.Command{
Name: "test",
Brief: "test brief",
Description: "test description 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
`,
Options: []gcmd.Option{
{
Name: "my-option",
Short: "o",
Brief: "It's my custom option",
NeedValue: false,
},
{
Name: "another",
Short: "a",
Brief: "It's my another custom option",
NeedValue: false,
},
},
Func: func(ctx context.Context, parser *gcmd.Parser) error {
fmt.Println("test")
return nil
},
}
err = commandRoot.Add(
commandEnv,
commandTest,
)
if err != nil {
g.Log().Fatal(ctx, err)
}
if err = commandRoot.Run(ctx); err != nil {
g.Log().Fatal(ctx, err)
}
})
}
func Test_Command_Print(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
ctx = gctx.New()
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, long brief.long brief.long brief.long brief.long brief.long brief.long brief.long brief.",
Description: "show current Golang environment variables",
Func: func(ctx context.Context, parser *gcmd.Parser) error {
return nil
},
}
if err = c.Add(commandEnv); err != nil {
g.Log().Fatal(ctx, 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(ctx context.Context, parser *gcmd.Parser) error {
return nil
},
}
if err = c.Add(commandGet); err != nil {
g.Log().Fatal(ctx, 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(ctx context.Context, parser *gcmd.Parser) error {
return nil
},
}
if err = c.Add(commandBuild); err != nil {
g.Log().Fatal(ctx, err)
}
c.Run(ctx)
})
}