delve/terminal/command.go
Konstantin Shaposhnikov 8c01bcc34d terminal/command: add optional linespec argument to clearall
If an optional linespec argument is passed to clearall command then only
breakpoints matching this linespec will be cleared.

Behavior of clearall command without arguments hasn't changed.
2015-10-04 10:49:25 -07:00

886 lines
23 KiB
Go

// Package command implements functions for responding to user
// input and dispatching to appropriate backend commands.
package terminal
import (
"bufio"
"fmt"
"math"
"os"
"regexp"
"sort"
"strconv"
"strings"
"text/tabwriter"
"github.com/derekparker/delve/service"
"github.com/derekparker/delve/service/api"
"github.com/derekparker/delve/service/debugger"
)
type cmdfunc func(t *Term, args ...string) error
type scopedCmdfunc func(t *Term, scope api.EvalScope, args ...string) error
type filteringFunc func(t *Term, filter string) ([]string, error)
type scopedFilteringFunc func(t *Term, scope api.EvalScope, filter string) ([]string, error)
type command struct {
aliases []string
helpMsg string
cmdFn cmdfunc
}
// Returns true if the command string matches one of the aliases for this command
func (c command) match(cmdstr string) bool {
for _, v := range c.aliases {
if v == cmdstr {
return true
}
}
return false
}
type Commands struct {
cmds []command
lastCmd cmdfunc
client service.Client
}
// Returns a Commands struct with default commands defined.
func DebugCommands(client service.Client) *Commands {
c := &Commands{client: client}
c.cmds = []command{
{aliases: []string{"help"}, cmdFn: c.help, helpMsg: "Prints the help message."},
{aliases: []string{"break", "b"}, cmdFn: breakpoint, helpMsg: "break <linespec> [-stack <n>|-goroutine|<variable name>]*"},
{aliases: []string{"trace", "t"}, cmdFn: tracepoint, helpMsg: "Set tracepoint, takes the same arguments as break."},
{aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: "Restart process."},
{aliases: []string{"continue", "c"}, cmdFn: cont, helpMsg: "Run until breakpoint or program termination."},
{aliases: []string{"step", "si"}, cmdFn: step, helpMsg: "Single step through program."},
{aliases: []string{"next", "n"}, cmdFn: next, helpMsg: "Step over to next source line."},
{aliases: []string{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."},
{aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: "Switch to the specified thread."},
{aliases: []string{"clear"}, cmdFn: clear, helpMsg: "Deletes breakpoint."},
{aliases: []string{"clearall"}, cmdFn: clearAll, helpMsg: "clearall [<linespec>]. Deletes all breakpoints matching linespec."},
{aliases: []string{"goroutines"}, cmdFn: goroutines, helpMsg: "Print out info for every goroutine."},
{aliases: []string{"goroutine"}, cmdFn: goroutine, helpMsg: "Sets current goroutine."},
{aliases: []string{"breakpoints", "bp"}, cmdFn: breakpoints, helpMsg: "Print out info for active breakpoints."},
{aliases: []string{"print", "p"}, cmdFn: g0f0(printVar), helpMsg: "Evaluate a variable."},
{aliases: []string{"set"}, cmdFn: g0f0(setVar), helpMsg: "Changes the value of a variable."},
{aliases: []string{"sources"}, cmdFn: filterSortAndOutput(sources), helpMsg: "Print list of source files, optionally filtered by a regexp."},
{aliases: []string{"funcs"}, cmdFn: filterSortAndOutput(funcs), helpMsg: "Print list of functions, optionally filtered by a regexp."},
{aliases: []string{"args"}, cmdFn: filterSortAndOutput(g0f0filter(args)), helpMsg: "Print function arguments, optionally filtered by a regexp."},
{aliases: []string{"locals"}, cmdFn: filterSortAndOutput(g0f0filter(locals)), helpMsg: "Print function locals, optionally filtered by a regexp."},
{aliases: []string{"vars"}, cmdFn: filterSortAndOutput(vars), helpMsg: "Print package variables, optionally filtered by a regexp."},
{aliases: []string{"regs"}, cmdFn: regs, helpMsg: "Print contents of CPU registers."},
{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: "Exit the debugger."},
{aliases: []string{"list", "ls"}, cmdFn: listCommand, helpMsg: "list <linespec>. Show source around current point or provided linespec."},
{aliases: []string{"stack", "bt"}, cmdFn: stackCommand, helpMsg: "stack [<depth>] [-full]. Prints stack."},
{aliases: []string{"frame"}, cmdFn: frame, helpMsg: "Sets current stack frame (0 is the top of the stack)"},
{aliases: []string{"source"}, cmdFn: c.sourceCommand, helpMsg: "Executes a file containing a list of delve commands"},
}
return c
}
// Register custom commands. Expects cf to be a func of type cmdfunc,
// returning only an error.
func (c *Commands) Register(cmdstr string, cf cmdfunc, helpMsg string) {
for _, v := range c.cmds {
if v.match(cmdstr) {
v.cmdFn = cf
return
}
}
c.cmds = append(c.cmds, command{aliases: []string{cmdstr}, cmdFn: cf, helpMsg: helpMsg})
}
// Find will look up the command function for the given command input.
// If it cannot find the command it will default to noCmdAvailable().
// If the command is an empty string it will replay the last command.
func (c *Commands) Find(cmdstr string) cmdfunc {
// If <enter> use last command, if there was one.
if cmdstr == "" {
if c.lastCmd != nil {
return c.lastCmd
}
return nullCommand
}
for _, v := range c.cmds {
if v.match(cmdstr) {
c.lastCmd = v.cmdFn
return v.cmdFn
}
}
return noCmdAvailable
}
// Merge takes aliases defined in the config struct and merges them with the default aliases.
func (c *Commands) Merge(allAliases map[string][]string) {
for i := range c.cmds {
if aliases, ok := allAliases[c.cmds[i].aliases[0]]; ok {
c.cmds[i].aliases = append(c.cmds[i].aliases, aliases...)
}
}
}
func CommandFunc(fn func() error) cmdfunc {
return func(t *Term, args ...string) error {
return fn()
}
}
func noCmdAvailable(t *Term, args ...string) error {
return fmt.Errorf("command not available")
}
func nullCommand(t *Term, args ...string) error {
return nil
}
func (c *Commands) help(t *Term, args ...string) error {
fmt.Println("The following commands are available:")
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 0, '-', 0)
for _, cmd := range c.cmds {
if len(cmd.aliases) > 1 {
fmt.Fprintf(w, " %s (alias: %s) \t %s\n", cmd.aliases[0], strings.Join(cmd.aliases[1:], " | "), cmd.helpMsg)
} else {
fmt.Fprintf(w, " %s \t %s\n", cmd.aliases[0], cmd.helpMsg)
}
}
return w.Flush()
}
type byThreadID []*api.Thread
func (a byThreadID) Len() int { return len(a) }
func (a byThreadID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byThreadID) Less(i, j int) bool { return a[i].ID < a[j].ID }
func threads(t *Term, args ...string) error {
threads, err := t.client.ListThreads()
if err != nil {
return err
}
state, err := t.client.GetState()
if err != nil {
return err
}
sort.Sort(byThreadID(threads))
for _, th := range threads {
prefix := " "
if state.CurrentThread != nil && state.CurrentThread.ID == th.ID {
prefix = "* "
}
if th.Function != nil {
fmt.Printf("%sThread %d at %#v %s:%d %s\n",
prefix, th.ID, th.PC, shortenFilePath(th.File),
th.Line, th.Function.Name)
} else {
fmt.Printf("%sThread %s\n", prefix, formatThread(th))
}
}
return nil
}
func thread(t *Term, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("you must specify a thread")
}
tid, err := strconv.Atoi(args[0])
if err != nil {
return err
}
oldState, err := t.client.GetState()
if err != nil {
return err
}
newState, err := t.client.SwitchThread(tid)
if err != nil {
return err
}
oldThread := "<none>"
newThread := "<none>"
if oldState.CurrentThread != nil {
oldThread = strconv.Itoa(oldState.CurrentThread.ID)
}
if newState.CurrentThread != nil {
newThread = strconv.Itoa(newState.CurrentThread.ID)
}
fmt.Printf("Switched from %s to %s\n", oldThread, newThread)
return nil
}
type byGoroutineID []*api.Goroutine
func (a byGoroutineID) Len() int { return len(a) }
func (a byGoroutineID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byGoroutineID) Less(i, j int) bool { return a[i].ID < a[j].ID }
func goroutines(t *Term, args ...string) error {
state, err := t.client.GetState()
if err != nil {
return err
}
gs, err := t.client.ListGoroutines()
if err != nil {
return err
}
sort.Sort(byGoroutineID(gs))
fmt.Printf("[%d goroutines]\n", len(gs))
for _, g := range gs {
prefix := " "
if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
prefix = "* "
}
fmt.Printf("%sGoroutine %s\n", prefix, formatGoroutine(g))
}
return nil
}
func goroutine(t *Term, args ...string) error {
switch len(args) {
case 0:
return printscope(t)
case 1:
gid, err := strconv.Atoi(args[0])
if err != nil {
return err
}
oldState, err := t.client.GetState()
if err != nil {
return err
}
newState, err := t.client.SwitchGoroutine(gid)
if err != nil {
return err
}
fmt.Printf("Switched from %d to %d (thread %d)\n", oldState.SelectedGoroutine.ID, gid, newState.CurrentThread.ID)
return nil
default:
return scopePrefix(t, "goroutine", args...)
}
}
func frame(t *Term, args ...string) error {
return scopePrefix(t, "frame", args...)
}
func scopePrefix(t *Term, cmdname string, pargs ...string) error {
fullargs := make([]string, 0, len(pargs)+1)
fullargs = append(fullargs, cmdname)
fullargs = append(fullargs, pargs...)
scope := api.EvalScope{-1, 0}
lastcmd := ""
callFilterSortAndOutput := func(fn scopedFilteringFunc, fnargs []string) error {
outfn := filterSortAndOutput(func(t *Term, filter string) ([]string, error) {
return fn(t, scope, filter)
})
return outfn(t, fnargs...)
}
for i := 0; i < len(fullargs); i++ {
lastcmd = fullargs[i]
switch fullargs[i] {
case "goroutine":
if i+1 >= len(fullargs) {
return fmt.Errorf("goroutine command needs an argument")
}
n, err := strconv.Atoi(fullargs[i+1])
if err != nil {
return fmt.Errorf("invalid argument to goroutine, expected integer")
}
scope.GoroutineID = int(n)
i++
case "frame":
if i+1 >= len(fullargs) {
return fmt.Errorf("frame command needs an argument")
}
n, err := strconv.Atoi(fullargs[i+1])
if err != nil {
return fmt.Errorf("invalid argument to frame, expected integer")
}
scope.Frame = int(n)
i++
case "list", "ls":
frame, gid := scope.Frame, scope.GoroutineID
locs, err := t.client.Stacktrace(gid, frame, false)
if err != nil {
return err
}
if frame >= len(locs) {
return fmt.Errorf("Frame %d does not exist in goroutine %d", frame, gid)
}
loc := locs[frame]
return printfile(t, loc.File, loc.Line, true)
case "stack", "bt":
depth, full, err := parseStackArgs(fullargs[i+1:])
if err != nil {
return err
}
stack, err := t.client.Stacktrace(scope.GoroutineID, depth, full)
if err != nil {
return err
}
printStack(stack, "")
return nil
case "locals":
return callFilterSortAndOutput(locals, fullargs[i+1:])
case "args":
return callFilterSortAndOutput(args, fullargs[i+1:])
case "print", "p":
return printVar(t, scope, fullargs[i+1:]...)
default:
return fmt.Errorf("unknown command %s", fullargs[i])
}
}
return fmt.Errorf("no command passed to %s", lastcmd)
}
func printscope(t *Term) error {
state, err := t.client.GetState()
if err != nil {
return err
}
fmt.Printf("Thread %s\nGoroutine %s\n", formatThread(state.CurrentThread), formatGoroutine(state.SelectedGoroutine))
return nil
}
func formatThread(th *api.Thread) string {
if th == nil {
return "<nil>"
}
return fmt.Sprintf("%d at %s:%d", th.ID, shortenFilePath(th.File), th.Line)
}
func formatGoroutine(g *api.Goroutine) string {
if g == nil {
return "<nil>"
}
fname := ""
if g.Function != nil {
fname = g.Function.Name
}
return fmt.Sprintf("%d - %s:%d %s (%#v)", g.ID, shortenFilePath(g.File), g.Line, fname, g.PC)
}
func restart(t *Term, args ...string) error {
if err := t.client.Restart(); err != nil {
return err
}
fmt.Println("Process restarted with PID", t.client.ProcessPid())
return nil
}
func cont(t *Term, args ...string) error {
stateChan := t.client.Continue()
for state := range stateChan {
if state.Err != nil {
return state.Err
}
printcontext(t, state)
}
return nil
}
func step(t *Term, args ...string) error {
state, err := t.client.Step()
if err != nil {
return err
}
printcontext(t, state)
return nil
}
func next(t *Term, args ...string) error {
state, err := t.client.Next()
if err != nil {
return err
}
printcontext(t, state)
return nil
}
func clear(t *Term, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments")
}
id, err := strconv.Atoi(args[0])
if err != nil {
return err
}
bp, err := t.client.ClearBreakpoint(id)
if err != nil {
return err
}
fmt.Printf("Breakpoint %d cleared at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, shortenFilePath(bp.File), bp.Line)
return nil
}
func clearAll(t *Term, args ...string) error {
breakPoints, err := t.client.ListBreakpoints()
if err != nil {
return err
}
var locPCs map[uint64]struct{}
if len(args) > 0 {
locs, err := t.client.FindLocation(api.EvalScope{-1, 0}, args[0])
if err != nil {
return err
}
locPCs = make(map[uint64]struct{})
for _, loc := range locs {
locPCs[loc.PC] = struct{}{}
}
}
for _, bp := range breakPoints {
if locPCs != nil {
if _, ok := locPCs[bp.Addr]; !ok {
continue
}
}
_, err := t.client.ClearBreakpoint(bp.ID)
if err != nil {
fmt.Printf("Couldn't delete breakpoint %d at %#v %s:%d: %s\n", bp.ID, bp.Addr, shortenFilePath(bp.File), bp.Line, err)
}
fmt.Printf("Breakpoint %d cleared at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, shortenFilePath(bp.File), bp.Line)
}
return nil
}
type ById []*api.Breakpoint
func (a ById) Len() int { return len(a) }
func (a ById) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ById) Less(i, j int) bool { return a[i].ID < a[j].ID }
func breakpoints(t *Term, args ...string) error {
breakPoints, err := t.client.ListBreakpoints()
if err != nil {
return err
}
sort.Sort(ById(breakPoints))
for _, bp := range breakPoints {
thing := "Breakpoint"
if bp.Tracepoint {
thing = "Tracepoint"
}
fmt.Printf("%s %d at %#v %s:%d\n", thing, bp.ID, bp.Addr, shortenFilePath(bp.File), bp.Line)
var attrs []string
if bp.Stacktrace > 0 {
attrs = append(attrs, "-stack")
attrs = append(attrs, strconv.Itoa(bp.Stacktrace))
}
if bp.Goroutine {
attrs = append(attrs, "-goroutine")
}
for i := range bp.Variables {
attrs = append(attrs, bp.Variables[i])
}
if len(attrs) > 0 {
fmt.Printf("\t%s\n", strings.Join(attrs, " "))
}
}
return nil
}
func setBreakpoint(t *Term, tracepoint bool, args ...string) error {
if len(args) < 1 {
return fmt.Errorf("address required, specify either a function name or <file:line>")
}
requestedBp := &api.Breakpoint{}
for i := 1; i < len(args); i++ {
switch args[i] {
case "-stack":
i++
n, err := strconv.Atoi(args[i])
if err != nil {
return fmt.Errorf("argument of -stack must be a number")
}
requestedBp.Stacktrace = n
case "-goroutine":
requestedBp.Goroutine = true
default:
requestedBp.Variables = append(requestedBp.Variables, args[i])
}
}
requestedBp.Tracepoint = tracepoint
locs, err := t.client.FindLocation(api.EvalScope{-1, 0}, args[0])
if err != nil {
return err
}
thing := "Breakpoint"
if tracepoint {
thing = "Tracepoint"
}
for _, loc := range locs {
requestedBp.Addr = loc.PC
bp, err := t.client.CreateBreakpoint(requestedBp)
if err != nil {
return err
}
fmt.Printf("%s %d set at %#v for %s %s:%d\n", thing, bp.ID, bp.Addr, bp.FunctionName, shortenFilePath(bp.File), bp.Line)
}
return nil
}
func breakpoint(t *Term, args ...string) error {
return setBreakpoint(t, false, args...)
}
func tracepoint(t *Term, args ...string) error {
return setBreakpoint(t, true, args...)
}
func g0f0(fn scopedCmdfunc) cmdfunc {
return func(t *Term, args ...string) error {
return fn(t, api.EvalScope{-1, 0}, args...)
}
}
func g0f0filter(fn scopedFilteringFunc) filteringFunc {
return func(t *Term, filter string) ([]string, error) {
return fn(t, api.EvalScope{-1, 0}, filter)
}
}
func printVar(t *Term, scope api.EvalScope, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments")
}
val, err := t.client.EvalVariable(scope, args[0])
if err != nil {
return err
}
fmt.Println(val.Value)
return nil
}
func setVar(t *Term, scope api.EvalScope, args ...string) error {
if len(args) != 2 {
return fmt.Errorf("wrong number of arguments")
}
return t.client.SetVariable(scope, args[0], args[1])
}
func filterVariables(vars []api.Variable, filter string) []string {
reg, err := regexp.Compile(filter)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
return nil
}
data := make([]string, 0, len(vars))
for _, v := range vars {
if reg == nil || reg.Match([]byte(v.Name)) {
data = append(data, fmt.Sprintf("%s = %s", v.Name, v.Value))
}
}
return data
}
func sources(t *Term, filter string) ([]string, error) {
return t.client.ListSources(filter)
}
func funcs(t *Term, filter string) ([]string, error) {
return t.client.ListFunctions(filter)
}
func args(t *Term, scope api.EvalScope, filter string) ([]string, error) {
vars, err := t.client.ListFunctionArgs(scope)
if err != nil {
return nil, err
}
return filterVariables(vars, filter), nil
}
func locals(t *Term, scope api.EvalScope, filter string) ([]string, error) {
locals, err := t.client.ListLocalVariables(scope)
if err != nil {
return nil, err
}
return filterVariables(locals, filter), nil
}
func vars(t *Term, filter string) ([]string, error) {
vars, err := t.client.ListPackageVariables(filter)
if err != nil {
return nil, err
}
return filterVariables(vars, filter), nil
}
func regs(t *Term, args ...string) error {
regs, err := t.client.ListRegisters()
if err != nil {
return err
}
fmt.Println(regs)
return nil
}
func filterSortAndOutput(fn filteringFunc) cmdfunc {
return func(t *Term, args ...string) error {
var filter string
if len(args) == 1 {
if _, err := regexp.Compile(args[0]); err != nil {
return fmt.Errorf("invalid filter argument: %s", err.Error())
}
filter = args[0]
}
data, err := fn(t, filter)
if err != nil {
return err
}
sort.Sort(sort.StringSlice(data))
for _, d := range data {
fmt.Println(d)
}
return nil
}
}
func stackCommand(t *Term, args ...string) error {
var (
err error
goroutineid = -1
)
depth, full, err := parseStackArgs(args)
if err != nil {
return err
}
stack, err := t.client.Stacktrace(goroutineid, depth, full)
if err != nil {
return err
}
printStack(stack, "")
return nil
}
func parseStackArgs(args []string) (int, bool, error) {
var (
depth = 10
full = false
)
for i := range args {
if args[i] == "-full" {
full = true
} else {
n, err := strconv.Atoi(args[i])
if err != nil {
return 0, false, fmt.Errorf("depth must be a number")
}
depth = n
}
}
return depth, full, nil
}
func listCommand(t *Term, args ...string) error {
if len(args) == 0 {
state, err := t.client.GetState()
if err != nil {
return err
}
printcontext(t, state)
return nil
}
locs, err := t.client.FindLocation(api.EvalScope{-1, 0}, args[0])
if err != nil {
return err
}
if len(locs) > 1 {
return debugger.AmbiguousLocationError{Location: args[0], CandidatesLocation: locs}
}
printfile(t, locs[0].File, locs[0].Line, false)
return nil
}
func (cmds *Commands) sourceCommand(t *Term, args ...string) error {
if len(args) != 1 {
return fmt.Errorf("wrong number of arguments: source <filename>")
}
return cmds.executeFile(t, args[0])
}
func digits(n int) int {
return int(math.Floor(math.Log10(float64(n)))) + 1
}
func printStack(stack []api.Stackframe, ind string) {
d := digits(len(stack) - 1)
fmtstr := "%s%" + strconv.Itoa(d) + "d 0x%016x in %s\n"
s := strings.Repeat(" ", d+2+len(ind))
for i := range stack {
name := "(nil)"
if stack[i].Function != nil {
name = stack[i].Function.Name
}
fmt.Printf(fmtstr, ind, i, stack[i].PC, name)
fmt.Printf("%sat %s:%d\n", s, shortenFilePath(stack[i].File), stack[i].Line)
for j := range stack[i].Arguments {
fmt.Printf("%s %s = %s\n", s, stack[i].Arguments[j].Name, stack[i].Arguments[j].Value)
}
for j := range stack[i].Locals {
fmt.Printf("%s %s = %s\n", s, stack[i].Locals[j].Name, stack[i].Locals[j].Value)
}
}
}
func printcontext(t *Term, state *api.DebuggerState) error {
if state.CurrentThread == nil {
fmt.Println("No current thread available")
return nil
}
if len(state.CurrentThread.File) == 0 {
fmt.Printf("Stopped at: 0x%x\n", state.CurrentThread.PC)
t.Println("=>", "no source available")
return nil
}
var fn *api.Function
if state.CurrentThread.Function != nil {
fn = state.CurrentThread.Function
}
if state.Breakpoint != nil && state.Breakpoint.Tracepoint {
var args []string
for _, arg := range state.CurrentThread.Function.Args {
args = append(args, arg.Value)
}
fmt.Printf("> %s(%s) %s:%d\n", fn.Name, strings.Join(args, ", "), shortenFilePath(state.CurrentThread.File), state.CurrentThread.Line)
} else {
fmt.Printf("> %s() %s:%d\n", fn.Name, shortenFilePath(state.CurrentThread.File), state.CurrentThread.Line)
}
if state.BreakpointInfo != nil {
bpi := state.BreakpointInfo
if bpi.Goroutine != nil {
fmt.Printf("\tGoroutine %s\n", formatGoroutine(bpi.Goroutine))
}
ss := make([]string, len(bpi.Variables))
for i, v := range bpi.Variables {
ss[i] = fmt.Sprintf("%s: <%v>", v.Name, v.Value)
}
fmt.Printf("\t%s\n", strings.Join(ss, ", "))
if bpi.Stacktrace != nil {
fmt.Printf("\tStack:\n")
printStack(bpi.Stacktrace, "\t\t")
}
}
if state.Breakpoint != nil && state.Breakpoint.Tracepoint {
return nil
}
return printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
}
func printfile(t *Term, filename string, line int, showArrow bool) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
buf := bufio.NewScanner(file)
l := line
for i := 1; i < l-5; i++ {
if !buf.Scan() {
return nil
}
}
s := l - 5
if s < 1 {
s = 1
}
for i := s; i <= l+5; i++ {
if !buf.Scan() {
return nil
}
var prefix string
if showArrow {
prefix = " "
if i == l {
prefix = "=>"
}
}
prefix = fmt.Sprintf("%s%4d:\t", prefix, i)
t.Println(prefix, buf.Text())
}
return nil
}
type ExitRequestError struct{}
func (ere ExitRequestError) Error() string {
return ""
}
func exitCommand(t *Term, args ...string) error {
return ExitRequestError{}
}
func shortenFilePath(fullPath string) string {
workingDir, _ := os.Getwd()
return strings.Replace(fullPath, workingDir, ".", 1)
}
func (cmds *Commands) executeFile(t *Term, name string) error {
fh, err := os.Open(name)
if err != nil {
return err
}
defer fh.Close()
scanner := bufio.NewScanner(fh)
lineno := 0
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
lineno++
if line == "" || line[0] == '#' {
continue
}
cmdstr, args := parseCommand(line)
cmd := cmds.Find(cmdstr)
err := cmd(t, args...)
if err != nil {
fmt.Printf("%s:%d: %v\n", name, lineno, err)
}
}
return scanner.Err()
}