diff --git a/app.go b/app.go index 9d88d7f..4f10c5d 100644 --- a/app.go +++ b/app.go @@ -105,12 +105,12 @@ func NewApp(fns ...func(app *App)) *App { Logf(VerbCrazy, "create a new cli application, and create base ") // init base + app.Ctx = gCtx app.base = newBase() app.opts = newGlobalOpts() // set a default version app.Version = "1.0.0" - app.Context = gCtx for _, fn := range fns { fn(app) @@ -151,7 +151,7 @@ func (app *App) initialize() { Logf(VerbCrazy, "initialize the cli application") // init some info - app.initHelpVars() + app.initHelpReplacer() app.bindAppOpts() // add default error handler. @@ -209,7 +209,7 @@ func (app *App) AddCommand(c *Command) { // init command c.app = app // inherit some from application - c.Context = app.Context + c.Ctx = app.Ctx // do add command app.addCommand(app.Name, c) @@ -472,7 +472,7 @@ func (app *App) Run(args []string) (code int) { return app.exitOnEnd(code) } - Logf(VerbCrazy, "begin run console application, PID: %d", app.PID()) + Logf(VerbCrazy, "begin run console application, PID: %d", app.Ctx.PID()) var name string code, name = app.prepareRun() @@ -528,9 +528,6 @@ func (app *App) doRunCmd(name string, args []string) (code int) { } func (app *App) doRunFunc(args []string) (code int) { - // app bind args TODO - // app.ParseArgs(args) - // do execute command if err := app.Func(app, args); err != nil { code = ERR diff --git a/app_test.go b/app_test.go index 519791d..56b0f42 100644 --- a/app_test.go +++ b/app_test.go @@ -22,7 +22,7 @@ var ( Desc: "an simple command", Func: func(c *gcli.Command, args []string) error { dump.Println(c.Path(), args) - c.Set("simple", "simple command") + c.Ctx.Set("simple", "simple command") return nil }, } @@ -45,7 +45,7 @@ var ( Name: "sub1", Desc: "desc for top1.sub1", Func: func(c *gcli.Command, args []string) error { - c.Set("msg", c.App().Get("top1:sub1")) + c.Ctx.Set("msg", c.App().Ctx.Get("top1:sub1")) return nil }, }, @@ -291,23 +291,23 @@ func TestApp_Run_subcommand(t *testing.T) { is := assert.New(t) id := "top1:sub1" - appWithMl.Set(id, "TestApp_Run_subcommand") + appWithMl.Ctx.Set(id, "TestApp_Run_subcommand") appWithMl.Run([]string{"top1", "sub1"}) c := appWithMl.FindCommand(id) is.NotEmpty(c) - is.Eq("TestApp_Run_subcommand", c.Get("msg")) + is.Eq("TestApp_Run_subcommand", c.Ctx.Get("msg")) } func TestApp_Run_by_cmd_ID(t *testing.T) { is := assert.New(t) - appWithMl.SetValue("top1:sub1", "TestApp_Run_by_cmd_ID") + appWithMl.Ctx.Set("top1:sub1", "TestApp_Run_by_cmd_ID") appWithMl.Run([]string{"top1:sub1"}) c := appWithMl.FindCommand("top1:sub1") is.NotEmpty(c) - is.Eq("TestApp_Run_by_cmd_ID", c.GetVal("msg")) + is.Eq("TestApp_Run_by_cmd_ID", c.Ctx.Get("msg")) } func TestApp_AddAliases_and_run(t *testing.T) { @@ -315,12 +315,12 @@ func TestApp_AddAliases_and_run(t *testing.T) { id := "top1:sub1" appWithMl.AddAliases(id, "ts1") - appWithMl.SetValue(id, "TestApp_AddAliases_and_run") + appWithMl.Ctx.Set(id, "TestApp_AddAliases_and_run") appWithMl.Run([]string{"ts1"}) c := appWithMl.FindCommand(id) is.NotEmpty(c) - is.Eq("TestApp_AddAliases_and_run", c.GetVal("msg")) + is.Eq("TestApp_AddAliases_and_run", c.Ctx.Get("msg")) } func TestApp_showCommandHelp(t *testing.T) { diff --git a/base.go b/base.go index 45a9483..3cb6a4d 100644 --- a/base.go +++ b/base.go @@ -10,7 +10,6 @@ import ( "strings" "github.com/gookit/color" - "github.com/gookit/gcli/v3/helper" "github.com/gookit/goutil/maputil" "github.com/gookit/goutil/structs" ) @@ -125,36 +124,27 @@ func (ctx *Context) hasHelpKeywords() bool { return strings.HasSuffix(ctx.argLine, " -h") || strings.HasSuffix(ctx.argLine, " --help") } -// SetValue to ctx -func (ctx *Context) SetValue(key string, val any) { - ctx.Set(key, val) -} - -// GetVal from ctx -func (ctx *Context) GetVal(key string) interface{} { - return ctx.Get(key) -} - // ResetData from ctx func (ctx *Context) ResetData() { ctx.Data = make(maputil.Data) } /************************************************************* - * command Base + * command base *************************************************************/ // will inject to every Command type base struct { + color.SimplePrinter // Hooks manage. allowed hooks: "init", "before", "after", "error" *Hooks - *Context - color.SimplePrinter - // HelpVars help message replace vars. - helper.HelpVars - // TODO tplVars for render help template text. - tplVars map[string]any + // HelpReplacer help message replace pairs. + HelpReplacer + // helpVars custom add vars for render help template. + helpVars map[string]any + // Ctx for command + Ctx *Context // Logo ASCII logo setting Logo *Logo // Version app version. like "1.0.1" @@ -200,25 +190,25 @@ func newBase() base { // cmdAliases: make(maputil.Aliases), cmdAliases: structs.NewAliases(aliasNameCheck), // ExitOnEnd: false, - tplVars: make(map[string]any), + helpVars: make(map[string]any), // Context: NewCtx(), } } // init common basic help vars -func (b *base) initHelpVars() { - b.AddVars(map[string]string{ - "pid": b.PIDString(), - "workDir": b.workDir, - "binFile": b.binFile, - "binName": b.binName, +func (b *base) initHelpReplacer() { + b.AddReplaces(map[string]string{ + "pid": b.Ctx.PIDString(), + "workDir": b.Ctx.workDir, + "binFile": b.Ctx.binFile, + "binName": b.Ctx.binName, }) } // ResetData from ctx func (b *base) ResetData() { - if b.Context != nil { - b.Context.ResetData() + if b.Ctx != nil { + b.Ctx.ResetData() } } @@ -392,7 +382,7 @@ func (b *base) AliasesMapping() map[string]string { return b.cmdAliases.Mapping() } -// AddTplVar to instance. -func (b *base) AddTplVar(key string, val any) { - b.tplVars[key] = val +// AddHelpVar to instance. +func (b *base) AddHelpVar(key string, val any) { + b.helpVars[key] = val } diff --git a/base_test.go b/base_test.go index c19e89f..866283b 100644 --- a/base_test.go +++ b/base_test.go @@ -115,7 +115,7 @@ func TestApp_On_CmdNotFound_redirect(t *testing.T) { buf.Reset() simpleCmd.Init() simpleCmd.ResetData() - assert.Eq(t, nil, simpleCmd.GetVal("simple")) + assert.Eq(t, nil, simpleCmd.Ctx.Get("simple")) cli := newNotExitApp() cli.Add(simpleCmd) @@ -128,7 +128,7 @@ func TestApp_On_CmdNotFound_redirect(t *testing.T) { err := ctx.App.Exec(simpleCmd.Name, nil) assert.NoErr(t, err) - buf.WriteString("value:" + simpleCmd.Data.Str("simple")) + buf.WriteString("value:" + simpleCmd.Ctx.Str("simple")) return true }) diff --git a/cmd.go b/cmd.go index c306b5c..4ec3964 100644 --- a/cmd.go +++ b/cmd.go @@ -178,9 +178,9 @@ func (c *Command) IsDisabled() bool { return c.disabled } -// Runnable reports whether the command can be run; otherwise +// IsRunnable reports whether the command can be run; otherwise // it is a documentation pseudo-command such as import path. -func (c *Command) Runnable() bool { +func (c *Command) IsRunnable() bool { return c.Func != nil } @@ -208,7 +208,7 @@ func (c *Command) AddCommand(sub *Command) { // inherit standalone value sub.standalone = c.standalone // inherit something from parent - sub.Context = c.Context + sub.Ctx = c.Ctx // initialize command c.initialize() @@ -291,22 +291,22 @@ func (c *Command) initCommandBase(cName string) { c.Hooks = &Hooks{} } - if c.Context == nil { + if c.Ctx == nil { Logf(VerbDebug, "cmd: %s - use the gCtx as command context", cName) - c.Context = gCtx + c.Ctx = gCtx } - binWithPath := c.binName + " " + c.Path() + binWithPath := c.Ctx.binName + " " + c.Path() - c.initHelpVars() - c.AddVars(map[string]string{ + c.initHelpReplacer() + c.AddReplaces(map[string]string{ "cmd": cName, // binName with command name "binWithCmd": binWithPath, // binName with command path "binWithPath": binWithPath, // binFile with command - "fullCmd": c.binFile + " " + cName, + "fullCmd": c.Ctx.binFile + " " + cName, }) c.base.cmdNames = make(map[string]int) diff --git a/cmd_test.go b/cmd_test.go index 66364d2..66fef7c 100644 --- a/cmd_test.go +++ b/cmd_test.go @@ -21,7 +21,7 @@ func TestNewCommand(t *testing.T) { }) is.NotEmpty(c) - is.False(c.Runnable()) + is.False(c.IsRunnable()) is.Nil(c.App()) err := c.Run([]string{}) @@ -55,7 +55,7 @@ func TestCommand_NewErrf(t *testing.T) { }) is.NotEmpty(c) - is.True(c.Runnable()) + is.True(c.IsRunnable()) err := c.Run(simpleArgs) is.Err(err) @@ -277,8 +277,8 @@ var c0 = gcli.NewCommand("test", "desc for test command", func(c *gcli.Command) c.AddArg("arg1", "arg1 desc") c.Func = func(c *gcli.Command, args []string) error { bf.WriteString("name=" + c.Name) - c.Set("name", c.Name) - c.Set("args", args) + c.Ctx.Set("name", c.Name) + c.Ctx.Set("args", args) // dump.P(c.ID(), "command Func is exec") return nil } @@ -357,8 +357,8 @@ func TestCommand_Run_parseOptions(t *testing.T) { // dump.P(gcli.GOpts(), c0.Context) is.NoErr(err) - is.Eq("test", c0.Get("name")) - is.Eq([]string{"txt"}, c0.Get("args")) + is.Eq("test", c0.Ctx.Get("name")) + is.Eq([]string{"txt"}, c0.Ctx.Get("args")) is.Eq(10, int0) is.Eq("abc", str0) diff --git a/hooks.go b/ext.go similarity index 70% rename from hooks.go rename to ext.go index 51a6667..1e3013a 100644 --- a/hooks.go +++ b/ext.go @@ -2,6 +2,8 @@ package gcli import ( "context" + "fmt" + "strings" "github.com/gookit/gcli/v3/events" "github.com/gookit/goutil/maputil" @@ -168,3 +170,62 @@ func (hc *HookCtx) WithApp(a *App) *HookCtx { hc.App = a return hc } + +/************************************************************* + * app/cmd help string-var replacer + *************************************************************/ + +// HelpVarFormat allow string replace on render help info. +// +// Default support: +// +// "{$binName}" "{$cmd}" "{$fullCmd}" "{$workDir}" +const HelpVarFormat = "{$%s}" + +// HelpReplacer provide string var replace for render help template. +type HelpReplacer struct { + VarOpen, VarClose string + + // replaces you can add string-var map for render help info. + replaces map[string]string +} + +// AddReplace get command name. AddReplace +func (hv *HelpReplacer) AddReplace(name, value string) { + if hv.replaces == nil { + hv.replaces = make(map[string]string) + } + hv.replaces[name] = value +} + +// AddReplaces add multi tpl vars. +func (hv *HelpReplacer) AddReplaces(vars map[string]string) { + for n, v := range vars { + hv.AddReplace(n, v) + } +} + +// GetReplace get a help var by name +func (hv *HelpReplacer) GetReplace(name string) string { + return hv.replaces[name] +} + +// Replaces get all tpl vars. +func (hv *HelpReplacer) Replaces() map[string]string { + return hv.replaces +} + +// ReplacePairs replace string vars in the input text. +func (hv *HelpReplacer) ReplacePairs(input string) string { + // if not use var + if !strings.Contains(input, "{$") { + return input + } + + var ss []string + for n, v := range hv.replaces { + ss = append(ss, fmt.Sprintf(HelpVarFormat, n), v) + } + + return strings.NewReplacer(ss...).Replace(input) +} diff --git a/ext_test.go b/ext_test.go new file mode 100644 index 0000000..7d2009a --- /dev/null +++ b/ext_test.go @@ -0,0 +1,32 @@ +package gcli_test + +import ( + "testing" + + "github.com/gookit/gcli/v3" + "github.com/gookit/goutil/testutil/assert" +) + +func TestHelpReplacer(t *testing.T) { + is := assert.New(t) + vs := gcli.HelpReplacer{} + + vs.AddReplaces(map[string]string{ + "key0": "val0", + "key1": "val1", + }) + + is.Len(vs.Replaces(), 2) + is.Contains(vs.Replaces(), "key0") + + vs.AddReplaces(map[string]string{"key2": "val2"}) + vs.AddReplace("key3", "val3") + + is.Eq("val3", vs.GetReplace("key3")) + is.Eq("", vs.GetReplace("not-exist")) + + is.Eq("hello val0", vs.ReplacePairs("hello {$key0}")) + is.Eq("hello val0 val2", vs.ReplacePairs("hello {$key0} {$key2}")) + // invalid input + is.Eq("hello {key0}", vs.ReplacePairs("hello {key0}")) +} diff --git a/gflag/opts.go b/gflag/opts.go index 23a2e7a..05cfe98 100644 --- a/gflag/opts.go +++ b/gflag/opts.go @@ -130,14 +130,16 @@ func (ops *CliOpts) Str(name, shorts string, defVal, desc string) *string { // StrVar binding an string option flag func (ops *CliOpts) StrVar(p *string, opt *CliOpt) { ops.strOpt(p, opt) } -// StrOpt binding an string option -func (ops *CliOpts) StrOpt(p *string, name, shorts string, defValWithDesc ...string) { +// StrOpt binding a string option. +// +// If defValAndDesc only one elem, will as desc message. +func (ops *CliOpts) StrOpt(p *string, name, shorts string, defValAndDesc ...string) { var defVal, desc string - if ln := len(defValWithDesc); ln > 0 { + if ln := len(defValAndDesc); ln > 0 { if ln >= 2 { - defVal, desc = defValWithDesc[0], defValWithDesc[1] + defVal, desc = defValAndDesc[0], defValAndDesc[1] } else { // only one as desc - desc = defValWithDesc[0] + desc = defValAndDesc[0] } } diff --git a/help.go b/help.go index 0de14e3..f0e019b 100644 --- a/help.go +++ b/help.go @@ -39,7 +39,7 @@ func (app *App) showCommandTips(name string) { color.Printf("\nMaybe you mean:\n %s\n", strings.Join(ns, ", ")) } - color.Printf("\nUse %s --help to see available commands\n", app.binName) + color.Printf("\nUse %s --help to see available commands\n", app.Ctx.binName) } // AppHelpTemplate help template for app(all commands) @@ -59,7 +59,7 @@ Use "{$binName} COMMAND -h" for more information about a command // display app help and list all commands. showCommandList() func (app *App) showApplicationHelp() bool { - Debugf("render application help and commands list, help.pairs=%s", maputil.ToString2(app.Vars)) + Debugf("render application help and commands list, replaces=%s", maputil.ToString2(app.Replaces())) // cmdHelpTemplate = color.ReplaceTag(cmdHelpTemplate) // render help text template @@ -71,6 +71,8 @@ func (app *App) showApplicationHelp() bool { "HasSubs": app.hasSubcommands, // always upper first char "Desc": strutil.UpperFirst(app.Desc), + // user custom help vars + "Vars": app.helpVars, }, template.FuncMap{ "paddingName": func(n string) string { return strutil.PadRight(n, " ", app.nameMaxWidth) @@ -78,13 +80,13 @@ func (app *App) showApplicationHelp() bool { }) // parse help vars and render color tags - color.Print(app.ReplaceVars(s)) + color.Print(app.ReplacePairs(s)) return false } -// showCommandHelp display help for an command +// showCommandHelp display help for a command func (app *App) showCommandHelp(list []string) (code int) { - binName := app.binName + binName := app.Ctx.binName // if len(list) == 0 { TODO support multi level sub command? if len(list) > 1 { color.Error.Tips("Too many arguments given.\n\nUsage: %s help COMMAND", binName) @@ -113,7 +115,7 @@ func (app *App) showCommandHelp(list []string) (code int) { } // show help for the give command. - cmd.ShowHelp() + _ = cmd.ShowHelp() return } @@ -223,6 +225,8 @@ func (c *Command) ShowHelp() (err error) { "Options": c.Flags.BuildOptsHelp(), // always upper first char "Desc": c.HelpDesc(), + // user custom help vars + "Vars": c.helpVars, } // if c.NotStandalone() { @@ -238,6 +242,6 @@ func (c *Command) ShowHelp() (err error) { // parse gcli help vars then print help // fmt.Printf("%#v\n", s) - color.Print(c.ReplaceVars(str)) + color.Print(c.ReplacePairs(str)) return } diff --git a/helper/help_vars.go b/helper/help_vars.go deleted file mode 100644 index e1c428a..0000000 --- a/helper/help_vars.go +++ /dev/null @@ -1,66 +0,0 @@ -package helper - -import ( - "fmt" - "strings" -) - -/************************************************************* - * app/cmd help vars - *************************************************************/ - -// HelpVarFormat allow var replace on render help info. -// -// Default support: -// -// "{$binName}" "{$cmd}" "{$fullCmd}" "{$workDir}" -const HelpVarFormat = "{$%s}" - -// HelpVars struct. provide string var function for render help template. -type HelpVars struct { - VarOpen, VarClose string - - // Vars you can add some vars map for render help info. TODO rename replaces/pairs - Vars map[string]string -} - -// AddVar get command name -func (hv *HelpVars) AddVar(name, value string) { - if hv.Vars == nil { - hv.Vars = make(map[string]string) - } - - hv.Vars[name] = value -} - -// AddVars add multi tpl vars -func (hv *HelpVars) AddVars(vars map[string]string) { - for n, v := range vars { - hv.AddVar(n, v) - } -} - -// GetVar get a help var by name -func (hv *HelpVars) GetVar(name string) string { - return hv.Vars[name] -} - -// GetVars get all tpl vars -func (hv *HelpVars) GetVars() map[string]string { - return hv.Vars -} - -// ReplaceVars replace vars in the input string. -func (hv *HelpVars) ReplaceVars(input string) string { - // if not use var - if !strings.Contains(input, "{$") { - return input - } - - var ss []string - for n, v := range hv.Vars { - ss = append(ss, fmt.Sprintf(HelpVarFormat, n), v) - } - - return strings.NewReplacer(ss...).Replace(input) -} diff --git a/helper/help_vars_test.go b/helper/help_vars_test.go deleted file mode 100644 index 0476e42..0000000 --- a/helper/help_vars_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package helper_test - -import ( - "testing" - - "github.com/gookit/gcli/v3/helper" - "github.com/gookit/goutil/testutil/assert" -) - -func TestHelpVars(t *testing.T) { - is := assert.New(t) - vs := helper.HelpVars{ - Vars: map[string]string{ - "key0": "val0", - "key1": "val1", - }, - } - - is.Len(vs.GetVars(), 2) - is.Contains(vs.GetVars(), "key0") - - vs.AddVars(map[string]string{"key2": "val2"}) - vs.AddVar("key3", "val3") - - is.Eq("val3", vs.GetVar("key3")) - is.Eq("", vs.GetVar("not-exist")) - - is.Eq("hello val0", vs.ReplaceVars("hello {$key0}")) - is.Eq("hello val0 val2", vs.ReplaceVars("hello {$key0} {$key2}")) - // invalid input - is.Eq("hello {key0}", vs.ReplaceVars("hello {key0}")) -}