Skip to content

Commit

Permalink
Feature: interact-mode with completion and history (#108)
Browse files Browse the repository at this point in the history
* use 'peterh/liner' in interact-mode
* support abbr completion in interact mode
* better completion in interact mode: realnames first, then abbrs
  • Loading branch information
innerr authored Dec 28, 2021
1 parent 8232a3a commit 8c1b214
Show file tree
Hide file tree
Showing 14 changed files with 236 additions and 65 deletions.
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ module github.com/pingcap/ticat

go 1.16

require github.com/mattn/go-shellwords v1.0.11
require (
github.com/mattn/go-shellwords v1.0.11
github.com/peterh/liner v1.2.1 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/YjEPnaw=
github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg=
github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
16 changes: 14 additions & 2 deletions pkg/builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,9 @@ func RegisterDbgCmds(cmds *core.CmdTree) {
AddArg("seconds", "3", "second", "sec", "s", "S")

breaks := cmds.AddSub("breaks", "break")
breaks.RegPowerCmd(DbgBreakAtBegin,
"set break point at the first command").
SetQuiet()

breaksAt := breaks.AddSub("at", "before").
RegPowerCmd(DbgBreakBefore,
Expand All @@ -723,6 +726,11 @@ func RegisterDbgCmds(cmds *core.CmdTree) {
"set break point at the first command").
SetQuiet()

breaksAt.AddSub("end", "finish").
RegPowerCmd(DbgBreakAtEnd,
"set break point after the last command").
SetQuiet()

breaks.AddSub("after", "post").
RegPowerCmd(DbgBreakAfter,
// TODO: get 'sep' from env or other config
Expand Down Expand Up @@ -753,8 +761,12 @@ func RegisterDbgCmds(cmds *core.CmdTree) {
"verify bash in os/exec").
SetQuiet()

cmds.AddSub("interact", "i", "I").
AddSub("leave", "l", "L").
interact := cmds.AddSub("interact", "interactive", "i", "I").
RegPowerCmd(DbgInteract,
"enter interact mode").
SetQuiet()

interact.AddSub("leave", "l", "L").
RegPowerCmd(DbgInteractLeave,
"leave interact mode and continue to run")

Expand Down
29 changes: 28 additions & 1 deletion pkg/builtin/dbg.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"strings"

"github.com/pingcap/ticat/pkg/cli/core"
//"github.com/pingcap/ticat/pkg/cli/display"
)

func DbgEcho(
Expand Down Expand Up @@ -88,6 +87,21 @@ func DbgBreakAtBegin(
return currCmdIdx, true
}

func DbgBreakAtEnd(
argv core.ArgVals,
cc *core.Cli,
env *core.Env,
flow *core.ParsedCmds,
currCmdIdx int) (int, bool) {

assertNotTailMode(flow, currCmdIdx)
cc.BreakPoints.SetAtEnd(true)
if 1 == len(flow.Cmds)-1 {
env.GetLayer(core.EnvLayerSession).SetBool("display.one-cmd", true)
}
return currCmdIdx, true
}

func DbgBreakBefore(
argv core.ArgVals,
cc *core.Cli,
Expand Down Expand Up @@ -131,3 +145,16 @@ func DbgInteractLeave(
env.SetBool("sys.interact.leaving", true)
return currCmdIdx, true
}

func DbgInteract(
argv core.ArgVals,
cc *core.Cli,
env *core.Env,
flow *core.ParsedCmds,
currCmdIdx int) (int, bool) {

assertNotTailMode(flow, currCmdIdx)

InteractiveMode(cc, env, "e")
return currCmdIdx, true
}
92 changes: 92 additions & 0 deletions pkg/builtin/interactive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package builtin

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/peterh/liner"

"github.com/pingcap/ticat/pkg/cli/core"
"github.com/pingcap/ticat/pkg/cli/display"
)

func InteractiveMode(cc *core.Cli, env *core.Env, exitStr string) {
cc.Screen.Print(display.ColorExplain("(ctl-c to leave)\n", env))

cc = cc.CopyForInteract()
sessionEnv := env.GetLayer(core.EnvLayerSession)
sessionEnv.SetBool("sys.interact.inside", true)

seqSep := env.GetRaw("strs.seq-sep")
selfName := env.GetRaw("strs.self-name")

lineReader := liner.NewLiner()
defer lineReader.Close()
lineReader.SetCtrlCAborts(true)

names := cc.Cmds.GatherNames()
lineReader.SetCompleter(func(line string) (res []string) {
fields := strings.Fields(line)
field := strings.TrimLeft(fields[len(fields)-1], seqSep)
prefix := line[0 : len(line)-len(field)]
for _, name := range names {
if strings.HasPrefix(name, field) {
res = append(res, prefix+name)
}
}
return
})

historyDir := filepath.Join(os.TempDir(), ".ticat_interact_mode_cmds_history")
if file, err := os.Open(historyDir); err == nil {
lineReader.ReadHistory(file)
file.Close()
}

for {
if env.GetBool("sys.interact.leaving") {
break
}
line, err := lineReader.Prompt(selfName + "> ")
if err == liner.ErrPromptAborted {
break
}
if err != nil {
panic(fmt.Errorf("[InteractMode] read from stdin failed: %v", err))
}

lineReader.AppendHistory(line)
executorSafeExecute("(interact)", cc, env, nil, core.FlowStrToStrs(line)...)
}

sessionEnv.GetLayer(core.EnvLayerSession).Delete("sys.interact.inside")

file, err := os.Create(historyDir)
if err != nil {
panic(fmt.Errorf("[InteractMode] error writing history file: %v", err))
}
lineReader.WriteHistory(file)
file.Close()
}

func executorSafeExecute(caller string, cc *core.Cli, env *core.Env, masks []*core.ExecuteMask, input ...string) {
env = env.GetLayer(core.EnvLayerSession)
stackDepth := env.GetRaw("sys.stack-depth")
stack := env.GetRaw("sys.stack")

defer func() {
env.Set("sys.stack-depth", stackDepth)
env.Set("sys.stack", stack)

if !env.GetBool("sys.panic.recover") {
return
}
if r := recover(); r != nil {
display.PrintError(cc, env, r.(error))
}
}()

cc.Executor.Execute(caller, false, cc, env, masks, input...)
}
23 changes: 16 additions & 7 deletions pkg/cli/core/breakpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,30 @@ import (
)

type BreakPoints struct {
Begin bool
AtBegin bool
AtEnd bool
Befores map[string]bool
Afters map[string]bool
}

func NewBreakPoints() *BreakPoints {
return &BreakPoints{false, map[string]bool{}, map[string]bool{}}
return &BreakPoints{false, false, map[string]bool{}, map[string]bool{}}
}

func (self *BreakPoints) SetAtBegin(enabled bool) {
self.Begin = enabled
self.AtBegin = enabled
}

func (self *BreakPoints) BreakAtBegin() bool {
return self.AtBegin
}

func (self *BreakPoints) SetAtEnd(enabled bool) {
self.AtEnd = enabled
}

func (self *BreakPoints) BreakAtEnd() bool {
return self.AtEnd
}

func (self *BreakPoints) SetBefores(cc *Cli, env *Env, cmdList []string) (verifiedCmds []string) {
Expand All @@ -38,10 +51,6 @@ func (self *BreakPoints) SetAfters(cc *Cli, env *Env, cmdList []string) (verifie
return
}

func (self *BreakPoints) AtBegin() bool {
return self.Begin
}

func (self *BreakPoints) BreakBefore(cmd string) bool {
return self.Befores[cmd]
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/core/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (self *ExecuteMask) Copy() *ExecuteMask {
}

type Executor interface {
Execute(caller string, cc *Cli, env *Env, masks []*ExecuteMask, input ...string) bool
Execute(caller string, inerrCall bool, cc *Cli, env *Env, masks []*ExecuteMask, input ...string) bool
Clone() Executor
}

Expand Down Expand Up @@ -73,7 +73,7 @@ func NewCli(screen Screen, cmds *CmdTree, parser CliParser, abbrs *EnvAbbrs, cmd

func (self *Cli) SetFlowStatusWriter(status *ExecutingFlow) {
if self.FlowStatus != nil {
panic(fmt.Errorf("[SetExecutingFlowStatusLogger] should never happen"))
panic(fmt.Errorf("[SetFlowStatusWriter] should never happen"))
}
self.FlowStatus = status
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/core/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ func (self *Cmd) executeFlow(argv ArgVals, cc *Cli, env *Env, mask *ExecuteMask)
masks = mask.SubFlow
}
if shouldExecByMask(mask) {
succeeded = cc.Executor.Execute(self.owner.DisplayPath(), cc, flowEnv, masks, flow...)
succeeded = cc.Executor.Execute(self.owner.DisplayPath(), true, cc, flowEnv, masks, flow...)
} else {
cc.Screen.Print("(skipped+)\n")
succeeded = true
Expand Down
27 changes: 27 additions & 0 deletions pkg/cli/core/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,33 @@ func (self *CmdTree) MatchWriteKey(key string) bool {
return self.cmd.MatchWriteKey(key)
}

// TODO: slow
func (self *CmdTree) GatherNames() (names []string) {
if self.parent == nil {
names = append(names, self.DisplayPath())
}
path := self.Path()
sep := self.Strs.PathSep

// Real names first, then abbrs
for sub, _ := range self.subs {
names = append(names, strings.Join(append(path, sub), sep))
}
for _, abbrs := range self.subAbbrs {
for _, abbr := range abbrs {
if _, ok := self.subs[abbr]; ok {
continue
}
names = append(names, strings.Join(append(path, abbr), sep))
}
}

for _, sub := range self.subs {
names = append(names, sub.GatherNames()...)
}
return names
}

func (self *CmdTree) RegCmd(cmd NormalCmd, help string) *Cmd {
self.cmdConflictCheck(help, "RegCmd")
self.cmd = NewCmd(self, help, cmd)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/display/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -725,10 +725,10 @@ func dumpExecutedModifiedEnv(
// TODO:
// return
}
if !args.ShowExecutedModifiedEnv && executedCmd.Result == core.ExecutedResultSucceeded {
if !args.ShowExecutedModifiedEnv && executedCmd.Result != core.ExecutedResultError {
return
}
if executedCmd.FinishEnv == nil && executedCmd.Result != core.ExecutedResultSucceeded {
if executedCmd.FinishEnv == nil && executedCmd.Result != core.ExecutedResultError {
return
}

Expand Down
20 changes: 12 additions & 8 deletions pkg/cli/display/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,19 @@ func PrintSwitchingThreadDisplay(preTid string, info core.BgTaskInfo, env *core.
func getFrameChars(env *core.Env) *FrameChars {
name := strings.ToLower(env.Get("display.style").Raw)
chars := getFrameCharsByName(env, name)
if env.GetInt("display.executor.displayed") == env.GetInt("sys.stack-depth") {
if env.GetBool("display.utf8") {
return FrameCharsAscii()
} else {
// TODO: have bugs
//return FrameCharsNoBorder()
return FrameCharsAscii()
// TODO: have bug
/*
// Display a lite frame under another frame
if env.GetInt("display.executor.displayed") == env.GetInt("sys.stack-depth") {
if env.GetBool("display.utf8") {
return FrameCharsAscii()
} else {
// TODO: have bugs
//return FrameCharsNoBorder()
return FrameCharsAscii()
}
}
}
*/
return chars
}

Expand Down
1 change: 1 addition & 0 deletions pkg/cli/execute/cmd_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func noSessionCmds(flow *core.ParsedCmds) bool {
}

funcs := []interface{}{
builtin.DbgInteract,
builtin.SessionRetry,
//builtin.LastSessionRetry,
}
Expand Down
16 changes: 11 additions & 5 deletions pkg/cli/execute/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ func (self *Executor) Run(cc *core.Cli, env *core.Env, bootstrap string, input .
return false
}
ok := self.execute(self.callerNameEntry, cc, env, nil, false, false, input...)

tryBreakAtEnd(cc, env)

builtin.WaitAllBgTasks(cc, env)
if cc.FlowStatus != nil {
cc.FlowStatus.OnFlowFinish(ok)
Expand All @@ -61,8 +64,8 @@ func (self *Executor) Run(cc *core.Cli, env *core.Env, bootstrap string, input .
}

// Implement core.Executor
func (self *Executor) Execute(caller string, cc *core.Cli, env *core.Env, masks []*core.ExecuteMask, input ...string) bool {
return self.execute(caller, cc, env, masks, false, true, input...)
func (self *Executor) Execute(caller string, innerCall bool, cc *core.Cli, env *core.Env, masks []*core.ExecuteMask, input ...string) bool {
return self.execute(caller, cc, env, masks, false, innerCall, input...)
}

func (self *Executor) execute(caller string, cc *core.Cli, env *core.Env, masks []*core.ExecuteMask,
Expand Down Expand Up @@ -108,7 +111,7 @@ func (self *Executor) execute(caller string, cc *core.Cli, env *core.Env, masks

display.PrintTolerableErrs(cc.Screen, env, cc.TolerableErrs)

if !innerCall && !bootstrap {
if !innerCall && !bootstrap && !env.GetBool("sys.interact.inside") {
noSession := noSessionCmds(flow)
if !noSession {
statusWriter, ok := core.SessionInit(cc, flow, env, self.sessionFileName, self.sessionStatusFileName)
Expand Down Expand Up @@ -393,8 +396,11 @@ func stackStepOut(caller string, callerNameEntry string, env *core.Env) {
if stack == callerNameEntry {
stack = ""
} else {
panic(fmt.Errorf("stack string not match when stepping out from '%s', stack: '%s'",
caller, stack))
fields := strings.Split(stack, sep)
if len(fields) != 1 || fields[0] != caller {
panic(fmt.Errorf("stack string not match when stepping out from '%s', stack: '%s'",
sep+caller, stack))
}
}
} else {
stack = stack[0 : len(stack)-len(sep)-len(caller)]
Expand Down
Loading

0 comments on commit 8c1b214

Please sign in to comment.