mirror of
https://gitee.com/johng/gf
synced 2026-06-06 02:25:47 +08:00
add argument feature for package gcmd
This commit is contained in:
@ -18,8 +18,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
CtxKeyParser gctx.StrKey = `CtxKeyParser`
|
||||
CtxKeyCommand gctx.StrKey = `CtxKeyCommand`
|
||||
CtxKeyParser gctx.StrKey = `CtxKeyParser`
|
||||
CtxKeyCommand gctx.StrKey = `CtxKeyCommand`
|
||||
CtxKeyArguments gctx.StrKey = `CtxKeyArguments`
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -20,13 +20,12 @@ type Command struct {
|
||||
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.
|
||||
Arguments []Argument // Argument array, configuring how this command act.
|
||||
Func Function // Custom function.
|
||||
FuncWithValue FuncWithValue // Custom function with output parameters that can interact with command caller.
|
||||
HelpFunc Function // Custom help function
|
||||
Examples string // Usage examples.
|
||||
Additional string // Additional info about this command, which will be appended to the end of help info.
|
||||
NeedArgs bool // NeedArgs specifies this command needs arguments.
|
||||
Strict bool // Strict parsing options, which means it returns error if invalid option given.
|
||||
Config string // Config node name, which also retrieves the values from config component along with command line.
|
||||
parent *Command // Parent command for internal usage.
|
||||
@ -39,18 +38,18 @@ type Function func(ctx context.Context, parser *Parser) (err error)
|
||||
// FuncWithValue is similar like Func but with output parameters that can interact with command caller.
|
||||
type FuncWithValue func(ctx context.Context, parser *Parser) (out interface{}, 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 {
|
||||
// Argument is the command value that are used by certain command.
|
||||
type Argument struct {
|
||||
Name string // Option name.
|
||||
Short string // Option short.
|
||||
Brief string // Brief info about this Option, which is used in help info.
|
||||
IsArg bool // IsArg marks this argument taking value from command line argument instead of option.
|
||||
Orphan bool // Whether this Option having or having no value bound to it.
|
||||
}
|
||||
|
||||
var (
|
||||
// defaultHelpOption is the default help option that will be automatically added to each command.
|
||||
defaultHelpOption = Option{
|
||||
defaultHelpOption = Argument{
|
||||
Name: `help`,
|
||||
Short: `h`,
|
||||
Brief: `more information about this command`,
|
||||
|
||||
@ -18,14 +18,14 @@ import (
|
||||
// Print prints help info to stdout for current command.
|
||||
func (c *Command) Print() {
|
||||
var (
|
||||
prefix = gstr.Repeat(" ", 4)
|
||||
buffer = bytes.NewBuffer(nil)
|
||||
options = make([]Option, len(c.Options))
|
||||
prefix = gstr.Repeat(" ", 4)
|
||||
buffer = bytes.NewBuffer(nil)
|
||||
arguments = make([]Argument, len(c.Arguments))
|
||||
)
|
||||
// Copy options for printing.
|
||||
copy(options, c.Options)
|
||||
copy(arguments, c.Arguments)
|
||||
// Add built-in help option, just for info only.
|
||||
options = append(options, defaultHelpOption)
|
||||
arguments = append(arguments, defaultHelpOption)
|
||||
|
||||
// Usage.
|
||||
if c.Usage != "" || c.Name != "" {
|
||||
@ -42,7 +42,11 @@ func (c *Command) Print() {
|
||||
name = p.parent.Name + " " + name
|
||||
p = p.parent
|
||||
}
|
||||
buffer.WriteString(fmt.Sprintf(`%s ARGUMENT [OPTION]`, name))
|
||||
if c.hasArgumentFromIndex() {
|
||||
buffer.WriteString(fmt.Sprintf(`%s ARGUMENT [OPTION]`, name))
|
||||
} else {
|
||||
buffer.WriteString(fmt.Sprintf(`%s [OPTION]`, name))
|
||||
}
|
||||
}
|
||||
buffer.WriteString("\n\n")
|
||||
}
|
||||
@ -73,32 +77,67 @@ func (c *Command) Print() {
|
||||
buffer.WriteString("\n")
|
||||
}
|
||||
|
||||
// Argument.
|
||||
if len(arguments) > 0 {
|
||||
buffer.WriteString("ARGUMENT\n")
|
||||
var (
|
||||
maxSpaceLength = 0
|
||||
)
|
||||
for _, arg := range arguments {
|
||||
if !arg.IsArg {
|
||||
continue
|
||||
}
|
||||
if len(arg.Name) > maxSpaceLength {
|
||||
maxSpaceLength = len(arg.Name)
|
||||
}
|
||||
}
|
||||
for _, arg := range arguments {
|
||||
if !arg.IsArg {
|
||||
continue
|
||||
}
|
||||
var (
|
||||
spaceLength = maxSpaceLength - len(arg.Name)
|
||||
lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, arg.Name, gstr.Repeat(" ", spaceLength+4), arg.Brief)
|
||||
wordwrapPrefix = gstr.Repeat(" ", len(prefix+arg.Name)+spaceLength+4)
|
||||
)
|
||||
lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n"+wordwrapPrefix)
|
||||
buffer.WriteString(lineStr)
|
||||
}
|
||||
buffer.WriteString("\n")
|
||||
}
|
||||
|
||||
// Option.
|
||||
if len(options) > 0 {
|
||||
if len(arguments) > 0 {
|
||||
buffer.WriteString("OPTION\n")
|
||||
var (
|
||||
nameStr string
|
||||
maxSpaceLength = 0
|
||||
)
|
||||
for _, option := range options {
|
||||
if option.Short != "" {
|
||||
nameStr = fmt.Sprintf("-%s,\t--%s", option.Short, option.Name)
|
||||
for _, arg := range arguments {
|
||||
if arg.IsArg {
|
||||
continue
|
||||
}
|
||||
if arg.Short != "" {
|
||||
nameStr = fmt.Sprintf("-%s,\t--%s", arg.Short, arg.Name)
|
||||
} else {
|
||||
nameStr = fmt.Sprintf("-/--%s", option.Name)
|
||||
nameStr = fmt.Sprintf("-/--%s", arg.Name)
|
||||
}
|
||||
if len(nameStr) > maxSpaceLength {
|
||||
maxSpaceLength = len(nameStr)
|
||||
}
|
||||
}
|
||||
for _, option := range options {
|
||||
if option.Short != "" {
|
||||
nameStr = fmt.Sprintf("-%s,\t--%s", option.Short, option.Name)
|
||||
for _, arg := range arguments {
|
||||
if arg.IsArg {
|
||||
continue
|
||||
}
|
||||
if arg.Short != "" {
|
||||
nameStr = fmt.Sprintf("-%s,\t--%s", arg.Short, arg.Name)
|
||||
} else {
|
||||
nameStr = fmt.Sprintf("-/--%s", option.Name)
|
||||
nameStr = fmt.Sprintf("-/--%s", arg.Name)
|
||||
}
|
||||
var (
|
||||
spaceLength = maxSpaceLength - len(nameStr)
|
||||
lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, nameStr, gstr.Repeat(" ", spaceLength+4), option.Brief)
|
||||
lineStr = fmt.Sprintf("%s%s%s%s\n", prefix, nameStr, gstr.Repeat(" ", spaceLength+4), arg.Brief)
|
||||
wordwrapPrefix = gstr.Repeat(" ", len(prefix+nameStr)+spaceLength+4)
|
||||
)
|
||||
lineStr = gstr.WordWrap(lineStr, maxLineChars, "\n"+wordwrapPrefix)
|
||||
|
||||
@ -27,7 +27,7 @@ const (
|
||||
tagNameDc = `dc`
|
||||
tagNameAd = `ad`
|
||||
tagNameEg = `eg`
|
||||
tagNameArgs = `args`
|
||||
tagNameArg = `arg`
|
||||
tagNameRoot = `root`
|
||||
)
|
||||
|
||||
@ -80,8 +80,8 @@ func NewFromObject(object interface{}) (rootCmd *Command, err error) {
|
||||
if rootCmd.FuncWithValue == nil {
|
||||
rootCmd.FuncWithValue = methodCommand.FuncWithValue
|
||||
}
|
||||
if len(rootCmd.Options) == 0 {
|
||||
rootCmd.Options = methodCommand.Options
|
||||
if len(rootCmd.Arguments) == 0 {
|
||||
rootCmd.Arguments = methodCommand.Arguments
|
||||
}
|
||||
} else {
|
||||
subCommands = append(subCommands, methodCommand)
|
||||
@ -115,9 +115,6 @@ func newCommandFromObjectMeta(object interface{}) (command *Command, err error)
|
||||
)
|
||||
return
|
||||
}
|
||||
if !command.NeedArgs {
|
||||
command.NeedArgs = gconv.Bool(metaData[tagNameArgs])
|
||||
}
|
||||
if command.Description == "" {
|
||||
command.Description = metaData[tagNameDc]
|
||||
}
|
||||
@ -201,11 +198,13 @@ func newCommandFromMethod(object interface{}, method reflect.Value) (command *Co
|
||||
}
|
||||
|
||||
// Options creating.
|
||||
if command.Options, err = newOptionsFromInput(inputObject.Interface()); err != nil {
|
||||
if command.Arguments, err = newArgumentsFromInput(inputObject.Interface()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// =============================================================================================
|
||||
// Create function that has value return.
|
||||
// =============================================================================================
|
||||
command.FuncWithValue = func(ctx context.Context, parser *Parser) (out interface{}, err error) {
|
||||
ctx = context.WithValue(ctx, CtxKeyParser, parser)
|
||||
|
||||
@ -221,15 +220,26 @@ func newCommandFromMethod(object interface{}, method reflect.Value) (command *Co
|
||||
|
||||
var (
|
||||
data = gconv.Map(parser.GetOptAll())
|
||||
argIndex = 0
|
||||
arguments = gconv.Strings(ctx.Value(CtxKeyArguments))
|
||||
inputValues = []reflect.Value{reflect.ValueOf(ctx)}
|
||||
)
|
||||
if data == nil {
|
||||
data = map[string]interface{}{}
|
||||
}
|
||||
// Handle orphan options.
|
||||
for _, option := range command.Options {
|
||||
if option.Orphan && parser.ContainsOpt(option.Name) {
|
||||
data[option.Name] = "true"
|
||||
for _, arg := range command.Arguments {
|
||||
if arg.IsArg {
|
||||
// Read argument from command line index.
|
||||
if argIndex < len(arguments) {
|
||||
data[arg.Name] = arguments[argIndex]
|
||||
argIndex++
|
||||
}
|
||||
} else {
|
||||
// Read argument from command line option name.
|
||||
if arg.Orphan && parser.ContainsOpt(arg.Name) {
|
||||
data[arg.Name] = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
// Default values from struct tag.
|
||||
@ -268,7 +278,7 @@ func newCommandFromMethod(object interface{}, method reflect.Value) (command *Co
|
||||
return
|
||||
}
|
||||
|
||||
func newOptionsFromInput(object interface{}) (options []Option, err error) {
|
||||
func newArgumentsFromInput(object interface{}) (args []Argument, err error) {
|
||||
var (
|
||||
fields []gstructs.Field
|
||||
)
|
||||
@ -278,28 +288,31 @@ func newOptionsFromInput(object interface{}) (options []Option, err error) {
|
||||
})
|
||||
for _, field := range fields {
|
||||
var (
|
||||
option = Option{}
|
||||
arg = Argument{}
|
||||
metaData = field.TagMap()
|
||||
)
|
||||
if err = gconv.Scan(metaData, &option); err != nil {
|
||||
if err = gconv.Scan(metaData, &arg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if option.Name == "" {
|
||||
option.Name = field.Name()
|
||||
if arg.Name == "" {
|
||||
arg.Name = field.Name()
|
||||
}
|
||||
if option.Name == helpOptionName {
|
||||
if arg.Name == helpOptionName {
|
||||
return nil, gerror.Newf(
|
||||
`option name "%s" is already token by built-in options`,
|
||||
option.Name,
|
||||
arg.Name,
|
||||
)
|
||||
}
|
||||
if option.Short == helpOptionNameShort {
|
||||
if arg.Short == helpOptionNameShort {
|
||||
return nil, gerror.Newf(
|
||||
`short option name "%s" is already token by built-in options`,
|
||||
option.Short,
|
||||
arg.Short,
|
||||
)
|
||||
}
|
||||
options = append(options, option)
|
||||
if v, ok := metaData[tagNameArg]; ok {
|
||||
arg.IsArg = gconv.Bool(v)
|
||||
}
|
||||
args = append(args, arg)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -40,8 +40,8 @@ func (c *Command) RunWithValue(ctx context.Context) (value interface{}, err erro
|
||||
args = args[1:]
|
||||
|
||||
// Find the matched command and run it.
|
||||
if subCommand := c.searchCommand(args); subCommand != nil {
|
||||
return subCommand.doRun(ctx, parser)
|
||||
if subCommand, newCtx := c.searchCommand(ctx, args); subCommand != nil {
|
||||
return subCommand.doRun(newCtx, parser)
|
||||
}
|
||||
|
||||
// Print error and help command if no command found.
|
||||
@ -86,20 +86,23 @@ func (c *Command) doRun(ctx context.Context, parser *Parser) (value interface{},
|
||||
|
||||
// reParse re-parses the arguments using option configuration of current command.
|
||||
func (c *Command) reParse(ctx context.Context, parser *Parser) (*Parser, error) {
|
||||
if len(c.Options) == 0 {
|
||||
if len(c.Arguments) == 0 {
|
||||
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
|
||||
for _, arg := range c.Arguments {
|
||||
if arg.IsArg {
|
||||
continue
|
||||
}
|
||||
supportedOptions[optionKey] = !option.Orphan
|
||||
if arg.Short != "" {
|
||||
optionKey = fmt.Sprintf(`%s,%s`, arg.Name, arg.Short)
|
||||
} else {
|
||||
optionKey = arg.Name
|
||||
}
|
||||
supportedOptions[optionKey] = !arg.Orphan
|
||||
}
|
||||
parser, err := Parse(supportedOptions, c.Strict)
|
||||
if err != nil {
|
||||
@ -128,24 +131,36 @@ func (c *Command) reParse(ctx context.Context, parser *Parser) (*Parser, error)
|
||||
}
|
||||
|
||||
// searchCommand recursively searches the command according given arguments.
|
||||
func (c *Command) searchCommand(args []string) *Command {
|
||||
func (c *Command) searchCommand(ctx context.Context, args []string) (*Command, context.Context) {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
return nil, ctx
|
||||
}
|
||||
for _, cmd := range c.commands {
|
||||
// If this command needs argument,
|
||||
// it then gives all its left arguments to it.
|
||||
if cmd.NeedArgs {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Recursively searching the command.
|
||||
if cmd.Name == args[0] {
|
||||
leftArgs := args[1:]
|
||||
if len(leftArgs) == 0 {
|
||||
return cmd
|
||||
// If this command needs argument,
|
||||
// it then gives all its left arguments to it.
|
||||
if cmd.hasArgumentFromIndex() {
|
||||
ctx = context.WithValue(ctx, CtxKeyArguments, leftArgs)
|
||||
return cmd, ctx
|
||||
}
|
||||
return cmd.searchCommand(leftArgs)
|
||||
// Recursively searching.
|
||||
if len(leftArgs) == 0 {
|
||||
return cmd, ctx
|
||||
}
|
||||
return cmd.searchCommand(ctx, leftArgs)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return nil, ctx
|
||||
}
|
||||
|
||||
func (c *Command) hasArgumentFromIndex() bool {
|
||||
for _, arg := range c.Arguments {
|
||||
if arg.IsArg {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@ -150,7 +150,9 @@ type TestObjectForNeedArgsEnvInput struct {
|
||||
type TestObjectForNeedArgsEnvOutput struct{}
|
||||
|
||||
type TestObjectForNeedArgsTestInput struct {
|
||||
g.Meta `name:"test" args:"true"`
|
||||
g.Meta `name:"test"`
|
||||
Arg1 string `arg:"true" brief:"arg1 for test command"`
|
||||
Arg2 string `arg:"true" brief:"arg2 for test command"`
|
||||
Name string `v:"required" short:"n" orphan:"false" brief:"name for test command"`
|
||||
}
|
||||
type TestObjectForNeedArgsTestOutput struct {
|
||||
@ -162,9 +164,8 @@ func (TestObjectForNeedArgs) Env(ctx context.Context, in TestObjectForNeedArgsEn
|
||||
}
|
||||
|
||||
func (TestObjectForNeedArgs) Test(ctx context.Context, in TestObjectForNeedArgsTestInput) (out *TestObjectForNeedArgsTestOutput, err error) {
|
||||
parser := gcmd.ParserFromCtx(ctx)
|
||||
out = &TestObjectForNeedArgsTestOutput{
|
||||
Args: parser.GetArgAll(),
|
||||
Args: []string{in.Arg1, in.Arg2, in.Name},
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -177,9 +178,13 @@ func Test_Command_NeedArgs(t *testing.T) {
|
||||
cmd, err := gcmd.NewFromObject(TestObjectForNeedArgs{})
|
||||
t.AssertNil(err)
|
||||
|
||||
//os.Args = []string{"root", "test", "a", "b", "c", "-h"}
|
||||
//value, err := cmd.RunWithValue(ctx)
|
||||
//t.AssertNil(err)
|
||||
|
||||
os.Args = []string{"root", "test", "a", "b", "c", "-n=john"}
|
||||
value, err := cmd.RunWithValue(ctx)
|
||||
t.AssertNil(err)
|
||||
t.Assert(value, `{"Args":["root","test","a","b","c"]}`)
|
||||
t.Assert(value, `{"Args":["a","b","john"]}`)
|
||||
})
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ gf get github.com/gogf/gf@latest
|
||||
gf get github.com/gogf/gf@master
|
||||
gf get golang.org/x/sys
|
||||
`,
|
||||
Options: []gcmd.Option{
|
||||
Arguments: []gcmd.Argument{
|
||||
{
|
||||
Name: "my-option",
|
||||
Short: "o",
|
||||
|
||||
Reference in New Issue
Block a user