Skip to content

Commit

Permalink
refactor: xcmd usage
Browse files Browse the repository at this point in the history
  • Loading branch information
hui.wang committed Jan 24, 2022
1 parent bfac758 commit 9143336
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 60 deletions.
101 changes: 65 additions & 36 deletions xcmd/commander.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@ package xcmd

import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"path"
"strings"

"github.com/sandwich-go/xconf"
"github.com/sandwich-go/xconf/xflag"
"github.com/sandwich-go/xconf/xutil"
)

Expand All @@ -34,25 +31,32 @@ func IsErrHelp(err error) bool {
// - Parser为Option传入的中间件,一般无需自行实现,Parser主要是将配置文件、FlagSet、Env等遵循XConf规则解析到Option传入的Bind对象上,如传nil,则会调用FlagSet的Parse方法
// - middleware执行的时候已经完成了参数对象的绑定解析
type Command struct {
name string
cc *config
Output io.Writer
commands []*Command
middleware []MiddlewareFunc
middlewarePre []MiddlewareFunc
name string
cc *config
Output io.Writer
commands []*Command

usageNamePath []string
parent *Command // 目前只用于确认命令是否已经有父节点

executer Executer
executerMiddleware []MiddlewareFunc
executerMiddleware []MiddlewareFunc // 记录设定Executer时的中间件,防止之后加入的中间件作用于Executer
executerMiddlewarePre []MiddlewareFunc

// 记录当前command上挂载的中间件
middleware []MiddlewareFunc
middlewarePre []MiddlewareFunc

bind interface{} // 命令绑定的参数结构
bindFieldPath []string // 命令绑定的参数FieldPath,如空则全部绑定

FlagArgs []string // 除去command名称的原始参数
FlagSet *flag.FlagSet
usage func()

// 缓存记录由parent继承而来的flag
flagInheritByMiddlewarePre []string
flagLocal []string
}

// NewCommand 创建一条命令
Expand All @@ -67,30 +71,52 @@ func newCommandWithConfig(name string, cc *config) *Command {
cc: cc,
Output: os.Stdout,
}
c.FlagSet = flag.NewFlagSet(name, flag.ContinueOnError)
c.usageNamePath = []string{name}
c.middlewarePre = append(c.middlewarePre, preMiddlewareBegin)
c.updateUsage(nil)
return c
}
func (c *Command) updateUsage(x *xconf.XConf) {
c.usage = func() {
c.Explain(c.Output)
fmt.Fprintf(c.Output, "Flags:\n")
if x == nil {
xflag.PrintDefaults(c.FlagSet)
} else {
x.UsageToWriter(c.Output, c.FlagArgs...)

func (c *Command) newXConf() *xconf.XConf {
cc := xconf.NewOptions(
xconf.WithErrorHandling(xconf.ContinueOnError),
xconf.WithFlagSet(c.FlagSet),
xconf.WithFlagArgs(c.FlagArgs...))
cc.ApplyOption(c.Config().GetXConfOption()...)
x := xconf.NewWithConf(cc)
return x
}

func preMiddlewareBegin(ctx context.Context, cmd *Command, next Executer) error {
cmd.FlagSet.VisitAll(func(f *flag.Flag) {
cmd.flagInheritByMiddlewarePre = append(cmd.flagInheritByMiddlewarePre, f.Name)
})
return next(ctx, cmd)
}

func preMiddlewareEnd(ctx context.Context, cmd *Command, next Executer) error {
var nowFlags []string
cmd.FlagSet.VisitAll(func(f *flag.Flag) {
nowFlags = append(nowFlags, f.Name)
})
for _, v := range nowFlags {
if xutil.ContainString(cmd.flagInheritByMiddlewarePre, v) {
continue
}
fmt.Fprintf(c.Output, "Use \"%s [command] --help\" for more information about a command.\n", path.Base(os.Args[0]))
cmd.flagLocal = append(cmd.flagLocal, v)
}
return next(ctx, cmd)
}

// Bind 获取绑定的对象
func (c *Command) Bind() interface{} { return c.bind }

// BindSet 设定参数绑定的对象,只在解析之前生效
// BindSet 设定参数绑定的对象,只在解析之前生效,并重置绑定
func (c *Command) BindSet(xconfVal interface{}) *Command {
c.bind = xconfVal
if c.bind == nil {
c.bindFieldPath = xconf.FieldPathList(c.bind, c.newXConf())
}
return c
}

Expand Down Expand Up @@ -163,18 +189,19 @@ func (c *Command) AddCommand(sub *Command, middleware ...MiddlewareFunc) {
panic("same command")
}
sub.usageNamePath = append(c.usageNamePath, sub.usageNamePath...)
sub.middleware = combineMiddlewareFunc(c.middleware, middleware...)

sub.middlewarePre = combineMiddlewareFunc(c.middlewarePre, sub.middlewarePre...)
sub.executerMiddleware = combineMiddlewareFunc(c.middleware, sub.executerMiddleware...)
sub.executerMiddlewarePre = combineMiddlewareFunc(c.middlewarePre, sub.executerMiddlewarePre...)
sub.middleware = combineMiddlewareFunc(c.middleware, middleware...)
sub.executerMiddleware = combineMiddlewareFunc(c.middleware, sub.executerMiddleware...)

sub.parent = c
// 如果该命令在添加子命令前没有父节点,则需要将父节点的中间件追加上
for _, v := range sub.commands {
v.middleware = combineMiddlewareFunc(c.middleware, v.middleware...)
v.middlewarePre = combineMiddlewareFunc(c.middlewarePre, v.middlewarePre...)
v.executerMiddleware = combineMiddlewareFunc(c.middleware, v.executerMiddleware...)
v.executerMiddlewarePre = combineMiddlewareFunc(c.middlewarePre, v.executerMiddlewarePre...)
v.middleware = combineMiddlewareFunc(c.middleware, v.middleware...)
v.executerMiddleware = combineMiddlewareFunc(c.middleware, v.executerMiddleware...)
}

c.commands = append(c.commands, sub)
Expand Down Expand Up @@ -216,6 +243,7 @@ func isFlagArg(arg string) bool {
// Execute 执行参数解析驱动命令执行
func (c *Command) Execute(ctx context.Context, args ...string) error {
// 存储原始的参数数据,主要debug使用
c.FlagSet = flag.NewFlagSet(c.name, flag.ContinueOnError)
c.FlagArgs = args
var argFirst string
if len(args) != 0 {
Expand All @@ -233,10 +261,12 @@ func (c *Command) Execute(ctx context.Context, args ...string) error {
var executerMiddleware []MiddlewareFunc
if c.executer == nil {
executerMiddleware = append(executerMiddleware, c.middlewarePre...)
executerMiddleware = append(executerMiddleware, preMiddlewareEnd)
executerMiddleware = append(executerMiddleware, parser)
executerMiddleware = append(executerMiddleware, c.middleware...)
} else {
executerMiddleware = append(executerMiddleware, c.executerMiddlewarePre...)
executerMiddleware = append(executerMiddleware, preMiddlewareEnd)
executerMiddleware = append(executerMiddleware, parser)
executerMiddleware = append(executerMiddleware, c.executerMiddleware...)
}
Expand Down Expand Up @@ -288,24 +318,23 @@ func (c *Command) Short() string { return c.cc.GetShort() }
func (c *Command) SubCommand(name string, opts ...ConfigOption) *Command {
config := NewConfig(WithXConfOption(c.cc.XConfOption...))
config.ApplyOption(opts...)
return newCommandWithConfig(name, config).
BindSet(c.bind).
BindFieldPathSet(c.bindFieldPath...).AddTo(c)
sub := newCommandWithConfig(name, config)
sub.bind = c.bind
sub.bindFieldPath = c.bindFieldPath
return sub.AddTo(c)
}

// Check 检查当前命令及子命令是否有路径绑定错误等信息
// Check 检查当前命令及子命令是否有路径绑定错误等信息. 调试使用
func (c *Command) Check() error {
for _, v := range c.commands {
binder := c.cc.GetParser()
if binder == nil {
return errors.New("need Parser")
}
err := binder(context.Background(), v, func(ctx context.Context, cmd *Command) error {
return nil
})
// 替换executer防止检查过程中的执行,输出
executer := v.executer
v.executer = func(ctx context.Context, cmd *Command) error { return nil }
err := v.Execute(context.Background())
if err != nil {
return err
}
v.executer = executer
}
return nil
}
Expand Down
140 changes: 133 additions & 7 deletions xcmd/explain.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package xcmd

import (
"flag"
"fmt"
"io"
"os"
"path"
"reflect"
"sort"
"strings"

"github.com/sandwich-go/xconf"
"github.com/sandwich-go/xconf/xflag"
"github.com/sandwich-go/xconf/xutil"
)

type byGroupName []*Command
Expand All @@ -19,22 +27,32 @@ func (c *Command) Explain(w io.Writer) { explainGroup(w, c) }
// explainGroup explains all the subcommands for a particular group.
func explainGroup(w io.Writer, c *Command) {
if len(c.commands) == 0 {
fmt.Fprintf(w, "Usage: \n%s <flags> <args>\n\n", strings.Join(c.usageNamePath, " "))
fmt.Fprintf(w, "USAGE: \n%s%s <flags> <args>\n\n", paddingContent, strings.Join(c.usageNamePath, " "))
} else {
fmt.Fprintf(w, "Usage: \n%s <subcommand> <flags> <args>\n\n", strings.Join(c.usageNamePath, " "))
fmt.Fprintf(w, "USAGE: \n%s%s <subcommand> <flags> <args>\n\n", paddingContent, strings.Join(c.usageNamePath, " "))
}
if len(c.commands) == 0 {
return
}
if c.cc.Usage != "" {
fmt.Fprintf(w, "%s\n\n", c.cc.Usage)
if c.cc.Description != "" {
fmt.Fprintf(w, "DESCRIPTION:\n")
usageLine := xutil.StringSliceWalk(strings.Split(xutil.StringTrim(c.cc.Description), "\n"), func(s string) (string, bool) {
return paddingContent + xutil.StringTrim(s), true
})
fmt.Fprintf(w, "%s\n\n", strings.Join(usageLine, "\n"))
}
if c.cc.Examples != "" {
fmt.Fprintf(w, "EXAMPLES:\n")
examplesLines := xutil.StringSliceWalk(strings.Split(xutil.StringTrim(c.cc.Examples), "\n"), func(s string) (string, bool) {
return paddingContent + xutil.StringTrim(s), true
})
fmt.Fprintf(w, "%s\n\n", strings.Join(examplesLines, "\n"))
}
sort.Sort(byGroupName(c.commands))
fmt.Fprintf(w, "Available Commands:\n")
fmt.Fprintf(w, "AVAIABLE COMMANDS:\n")
sort.Sort(byGroupName(c.commands))
var level = []bool{}
lines := printCommand(c, level)
// lines = xutil.TableFormatLines(lines, magic)
fmt.Fprintln(w, strings.Join(lines, "\n"))
fmt.Fprintf(w, "\n")
}
Expand Down Expand Up @@ -63,6 +81,7 @@ func getPrefix(lvl []bool) string {
}

const padding = 4
const paddingContent = " "

func applyPadding(filler string) string {
var fill string
Expand All @@ -75,7 +94,7 @@ func applyPadding(filler string) string {
const magic = "\x00"

func printCommand(c *Command, lvl []bool) (lines []string) {
lines = append(lines, fmt.Sprintf("%s%s(%d,%d) %s %s", getPrefix(lvl), c.name, len(c.middlewarePre), len(c.middleware), magic, c.cc.GetShort()))
lines = append(lines, fmt.Sprintf("%s%s%s(%d,%d) %s %s", paddingContent, getPrefix(lvl), c.name, len(c.middlewarePre), len(c.middleware), magic, c.cc.GetShort()))
var level = append(lvl, false)
for i := 0; i < len(c.commands); i++ {
if i+1 == len(c.commands) {
Expand All @@ -86,3 +105,110 @@ func printCommand(c *Command, lvl []bool) (lines []string) {
}
return lines
}

func (c *Command) updateUsage(x *xconf.XConf) {
c.usage = func() {
c.Explain(c.Output)
var bindFieldPathParent []string
bindFieldPath := c.bindFieldPath
if c.parent != nil {
bindFieldPathParent = c.parent.bindFieldPath
if c.parent.bind != nil && len(bindFieldPathParent) == 0 {
bindFieldPathParent = xconf.FieldPathList(c.parent.bind, c.parent.newXConf())
}
}

if c.bind != nil && len(bindFieldPath) == 0 {
bindFieldPath = xconf.FieldPathList(c.bind, x)
}
local := c.flagLocal
for _, v := range bindFieldPath {
if xutil.ContainString(bindFieldPathParent, v) {
continue
}
local = append(local, v)
}
var nowFlags []string
c.FlagSet.VisitAll(func(f *flag.Flag) {
nowFlags = append(nowFlags, f.Name)
})
var inherit []string
for _, v := range nowFlags {
if xutil.ContainString(local, v) {
continue
}
inherit = append(inherit, v)
}

allFlag := xflag.GetFlagInfo(c.FlagSet)
fieldPathInfoMap := make(map[string]xconf.StructFieldPathInfo)
if c.bind != nil {
fieldPathInfoMap = x.ZeroStructKeysTagList(reflect.New(reflect.ValueOf(c.bind).Type().Elem()).Interface())
}

var linesGlobal []string
var linesLocal []string
magic := "\x00"
for _, v := range allFlag.List {
line := fmt.Sprintf("--%s", v.Name)
line += magic
tag := "-"
if c.bind != nil {
tag = xconf.FlagTypeStr(x, v.Name)
}
line += v.TypeName
line += magic
usage := ""
if info, ok := fieldPathInfoMap[v.Name]; ok {
usage = info.Tag.Get("usage")
}
if usage == "" {
usage = v.Usage
}
line += fmt.Sprintf("|%s| %s", tag, usage)
if xutil.ContainString(inherit, v.Name) {
linesGlobal = append(linesGlobal, line)
} else if xutil.ContainString(local, v.Name) {
linesLocal = append(linesLocal, line)
} else {
panic("invalid flag name : " + v.Name)
}
}
heaerLine := "FLAG" + "\x00" + "TYPE" + "\x00" + "USAGE"
var allLine []string
allLine = append(allLine, heaerLine)
allLine = append(allLine, linesGlobal...)
allLine = append(allLine, linesLocal...)
lineAllFormatted := xutil.TableFormatLines(allLine, magic)
lineMaxLen := xutil.StringMaxLenByRune(lineAllFormatted)

if len(linesGlobal) > 0 {
fmt.Fprintf(c.Output, "OPTIONS GLOBAL:\n")
fmt.Fprintln(c.Output, paddingContent+strings.Repeat("-", lineMaxLen))
fmt.Fprintln(c.Output, paddingContent+lineAllFormatted[0])
fmt.Fprintln(c.Output, paddingContent+strings.Repeat("-", lineMaxLen))
sorted := lineAllFormatted[1 : len(linesGlobal)+1]
sort.Strings(sorted)
for i := 0; i < len(linesGlobal); i++ {
fmt.Fprintln(c.Output, paddingContent+sorted[i])
}

fmt.Fprintln(c.Output, paddingContent+strings.Repeat("-", lineMaxLen))
fmt.Fprintln(c.Output)
}

if len(linesLocal) > 0 {
fmt.Fprintf(c.Output, "OPTIONS LOCAL:\n")
fmt.Fprintln(c.Output, paddingContent+strings.Repeat("-", lineMaxLen))
fmt.Fprintln(c.Output, paddingContent+lineAllFormatted[0])
fmt.Fprintln(c.Output, paddingContent+strings.Repeat("-", lineMaxLen))
sorted := lineAllFormatted[1+len(linesGlobal):]
for i := 0; i < len(linesLocal); i++ {
fmt.Fprintln(c.Output, paddingContent+sorted[i])
}
fmt.Fprintln(c.Output, paddingContent+strings.Repeat("-", lineMaxLen))
}
fmt.Fprintln(c.Output)
fmt.Fprintf(c.Output, "Use \"%s [command] --help\" for more information about a command.\n", path.Base(os.Args[0]))
}
}
Loading

0 comments on commit 9143336

Please sign in to comment.