command.go 16.2 KB
Newer Older
D
Dan Mace 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13
// Package command implements functions for responding to user
// input and dispatching to appropriate backend commands.
package terminal

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"regexp"
	"sort"
	"strconv"
	"strings"
D
Derek Parker 已提交
14
	"text/tabwriter"
D
Dan Mace 已提交
15 16 17

	"github.com/derekparker/delve/service"
	"github.com/derekparker/delve/service/api"
18
	"github.com/derekparker/delve/service/debugger"
D
Dan Mace 已提交
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
)

type cmdfunc func(client service.Client, args ...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."},
51
		{aliases: []string{"break", "b"}, cmdFn: breakpoint, helpMsg: "break <linespec> [-stack <n>|-goroutine|<variable name>]*"},
D
Derek Parker 已提交
52
		{aliases: []string{"trace", "t"}, cmdFn: tracepoint, helpMsg: "Set tracepoint, takes the same arguments as break."},
D
Derek Parker 已提交
53
		{aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: "Restart process."},
D
Dan Mace 已提交
54 55 56 57
		{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."},
D
Derek Parker 已提交
58
		{aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: "Switch to the specified thread."},
D
Dan Mace 已提交
59 60 61 62 63
		{aliases: []string{"clear"}, cmdFn: clear, helpMsg: "Deletes breakpoint."},
		{aliases: []string{"clearall"}, cmdFn: clearAll, helpMsg: "Deletes all breakpoints."},
		{aliases: []string{"goroutines"}, cmdFn: goroutines, helpMsg: "Print out info for every goroutine."},
		{aliases: []string{"breakpoints", "bp"}, cmdFn: breakpoints, helpMsg: "Print out info for active breakpoints."},
		{aliases: []string{"print", "p"}, cmdFn: printVar, helpMsg: "Evaluate a variable."},
D
Derek Parker 已提交
64 65 66 67 68 69
		{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(args), helpMsg: "Print function arguments, optionally filtered by a regexp."},
		{aliases: []string{"locals"}, cmdFn: filterSortAndOutput(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."},
D
Derek Parker 已提交
70
		{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: "Exit the debugger."},
71
		{aliases: []string{"stack", "bt"}, cmdFn: stackCommand, helpMsg: "stack [<depth> [<goroutine id>]]. Prints stack."},
72
		{aliases: []string{"list"}, cmdFn: listCommand, helpMsg: "list <linespec>.  Show source around current point or provided linespec."},
D
Dan Mace 已提交
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
	}

	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.
D
Derek Parker 已提交
92
// If it cannot find the command it will default to noCmdAvailable().
D
Dan Mace 已提交
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
// 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
}

func CommandFunc(fn func() error) cmdfunc {
	return func(client service.Client, args ...string) error {
		return fn()
	}
}

func noCmdAvailable(client service.Client, args ...string) error {
D
Derek Parker 已提交
120
	return fmt.Errorf("command not available")
D
Dan Mace 已提交
121 122 123 124 125 126 127 128
}

func nullCommand(client service.Client, args ...string) error {
	return nil
}

func (c *Commands) help(client service.Client, args ...string) error {
	fmt.Println("The following commands are available:")
D
Derek Parker 已提交
129 130
	w := new(tabwriter.Writer)
	w.Init(os.Stdout, 0, 8, 0, '-', 0)
D
Dan Mace 已提交
131
	for _, cmd := range c.cmds {
D
Derek Parker 已提交
132 133 134 135 136
		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)
		}
D
Dan Mace 已提交
137
	}
D
Derek Parker 已提交
138
	return w.Flush()
D
Dan Mace 已提交
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
}

func threads(client service.Client, args ...string) error {
	threads, err := client.ListThreads()
	if err != nil {
		return err
	}
	state, err := client.GetState()
	if err != nil {
		return err
	}
	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, th.File,
				th.Line, th.Function.Name)
		} else {
			fmt.Printf("%sThread %d at %s:%d\n", prefix, th.ID, th.File, th.Line)
		}
	}
	return nil
}

func thread(client service.Client, args ...string) error {
167 168 169
	if len(args) == 0 {
		return fmt.Errorf("you must specify a thread")
	}
D
Dan Mace 已提交
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
	tid, err := strconv.Atoi(args[0])
	if err != nil {
		return err
	}

	oldState, err := client.GetState()
	if err != nil {
		return err
	}

	newState, err := 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
}

func goroutines(client service.Client, args ...string) error {
	gs, err := client.ListGoroutines()
	if err != nil {
		return err
	}
	fmt.Printf("[%d goroutines]\n", len(gs))
	for _, g := range gs {
A
aarzilli 已提交
205
		fmt.Printf("Goroutine %s\n", formatGoroutine(g))
D
Dan Mace 已提交
206 207 208 209
	}
	return nil
}

A
aarzilli 已提交
210 211 212 213 214 215 216 217
func formatGoroutine(g *api.Goroutine) string {
	fname := ""
	if g.Function != nil {
		fname = g.Function.Name
	}
	return fmt.Sprintf("%d - %s:%d %s (%#v)\n", g.ID, g.File, g.Line, fname, g.PC)
}

D
Derek Parker 已提交
218 219 220 221 222 223 224 225
func restart(client service.Client, args ...string) error {
	if err := client.Restart(); err != nil {
		return err
	}
	fmt.Println("Process restarted with PID", client.ProcessPid())
	return nil
}

D
Dan Mace 已提交
226
func cont(client service.Client, args ...string) error {
D
Derek Parker 已提交
227 228
	stateChan := client.Continue()
	for state := range stateChan {
A
aarzilli 已提交
229 230 231 232
		if state.Err != nil {
			return state.Err
		}
		printcontext(state)
D
Dan Mace 已提交
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
	}
	return nil
}

func step(client service.Client, args ...string) error {
	state, err := client.Step()
	if err != nil {
		return err
	}
	printcontext(state)
	return nil
}

func next(client service.Client, args ...string) error {
	state, err := client.Next()
	if err != nil {
		return err
	}
	printcontext(state)
	return nil
}

func clear(client service.Client, args ...string) error {
	if len(args) == 0 {
D
Derek Parker 已提交
257
		return fmt.Errorf("not enough arguments")
D
Dan Mace 已提交
258 259 260 261 262 263 264
	}

	id, err := strconv.Atoi(args[0])
	if err != nil {
		return err
	}

D
Derek Parker 已提交
265
	bp, err := client.ClearBreakpoint(id)
D
Dan Mace 已提交
266 267 268 269 270 271 272 273
	if err != nil {
		return err
	}
	fmt.Printf("Breakpoint %d cleared at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, bp.File, bp.Line)
	return nil
}

func clearAll(client service.Client, args ...string) error {
D
Derek Parker 已提交
274
	breakPoints, err := client.ListBreakpoints()
D
Dan Mace 已提交
275 276 277 278
	if err != nil {
		return err
	}
	for _, bp := range breakPoints {
D
Derek Parker 已提交
279
		_, err := client.ClearBreakpoint(bp.ID)
D
Dan Mace 已提交
280 281 282 283 284 285 286 287
		if err != nil {
			fmt.Printf("Couldn't delete breakpoint %d at %#v %s:%d: %s\n", bp.ID, bp.Addr, bp.File, bp.Line, err)
		}
		fmt.Printf("Breakpoint %d cleared at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, bp.File, bp.Line)
	}
	return nil
}

D
Derek Parker 已提交
288
type ById []*api.Breakpoint
D
Dan Mace 已提交
289 290 291 292 293 294

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(client service.Client, args ...string) error {
D
Derek Parker 已提交
295
	breakPoints, err := client.ListBreakpoints()
D
Dan Mace 已提交
296 297 298 299 300
	if err != nil {
		return err
	}
	sort.Sort(ById(breakPoints))
	for _, bp := range breakPoints {
A
aarzilli 已提交
301 302 303 304 305 306 307 308 309 310 311 312 313 314
		thing := "Breakpoint"
		if bp.Tracepoint {
			thing = "Tracepoint"
		}
		fmt.Printf("%s %d at %#v %s:%d\n", thing, bp.ID, bp.Addr, 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")
		}
D
Derek Parker 已提交
315 316
		for i := range bp.Variables {
			attrs = append(attrs, bp.Variables[i])
A
aarzilli 已提交
317 318 319 320 321
		}

		if len(attrs) > 0 {
			fmt.Printf("\t%s\n", strings.Join(attrs, " "))
		}
D
Dan Mace 已提交
322 323 324 325 326
	}

	return nil
}

D
Derek Parker 已提交
327
func setBreakpoint(client service.Client, tracepoint bool, args ...string) error {
A
aarzilli 已提交
328 329
	if len(args) < 1 {
		return fmt.Errorf("address required, specify either a function name or <file:line>")
D
Dan Mace 已提交
330
	}
A
aarzilli 已提交
331

D
Derek Parker 已提交
332
	requestedBp := &api.Breakpoint{}
D
Dan Mace 已提交
333

A
aarzilli 已提交
334 335 336 337 338 339 340 341 342 343 344 345
	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:
D
Derek Parker 已提交
346
			requestedBp.Variables = append(requestedBp.Variables, args[i])
A
aarzilli 已提交
347 348 349 350 351
		}
	}

	requestedBp.Tracepoint = tracepoint

352
	locs, err := client.FindLocation(args[0])
D
Dan Mace 已提交
353 354 355 356
	if err != nil {
		return err
	}

A
aarzilli 已提交
357 358 359 360 361
	thing := "Breakpoint"
	if tracepoint {
		thing = "Tracepoint"
	}

362 363 364 365 366 367 368 369 370 371 372
	for _, loc := range locs {
		requestedBp.Addr = loc.PC

		bp, err := 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, bp.File, bp.Line)
	}

D
Dan Mace 已提交
373 374 375
	return nil
}

A
aarzilli 已提交
376
func breakpoint(client service.Client, args ...string) error {
D
Derek Parker 已提交
377
	return setBreakpoint(client, false, args...)
A
aarzilli 已提交
378 379 380
}

func tracepoint(client service.Client, args ...string) error {
D
Derek Parker 已提交
381
	return setBreakpoint(client, true, args...)
A
aarzilli 已提交
382 383
}

D
Dan Mace 已提交
384 385
func printVar(client service.Client, args ...string) error {
	if len(args) == 0 {
D
Derek Parker 已提交
386
		return fmt.Errorf("not enough arguments")
D
Dan Mace 已提交
387 388
	}

389
	val, err := client.EvalVariable(args[0])
D
Dan Mace 已提交
390 391 392 393 394 395 396 397
	if err != nil {
		return err
	}

	fmt.Println(val.Value)
	return nil
}

D
Derek Parker 已提交
398 399 400 401 402 403
func filterVariables(vars []api.Variable, filter string) []string {
	reg, err := regexp.Compile(filter)
	if err != nil {
		fmt.Fprintf(os.Stderr, err.Error())
		return nil
	}
D
Dan Mace 已提交
404 405
	data := make([]string, 0, len(vars))
	for _, v := range vars {
D
Derek Parker 已提交
406
		if reg == nil || reg.Match([]byte(v.Name)) {
D
Dan Mace 已提交
407 408 409 410 411 412
			data = append(data, fmt.Sprintf("%s = %s", v.Name, v.Value))
		}
	}
	return data
}

D
Derek Parker 已提交
413 414 415
func sources(client service.Client, filter string) ([]string, error) {
	return client.ListSources(filter)
}
D
Dan Mace 已提交
416

D
Derek Parker 已提交
417 418 419
func funcs(client service.Client, filter string) ([]string, error) {
	return client.ListFunctions(filter)
}
D
Dan Mace 已提交
420

D
Derek Parker 已提交
421 422 423 424 425 426 427
func args(client service.Client, filter string) ([]string, error) {
	vars, err := client.ListFunctionArgs()
	if err != nil {
		return nil, err
	}
	return filterVariables(vars, filter), nil
}
D
Dan Mace 已提交
428

D
Derek Parker 已提交
429 430 431 432 433 434 435
func locals(client service.Client, filter string) ([]string, error) {
	locals, err := client.ListLocalVariables()
	if err != nil {
		return nil, err
	}
	return filterVariables(locals, filter), nil
}
D
Dan Mace 已提交
436

D
Derek Parker 已提交
437 438 439 440 441 442 443
func vars(client service.Client, filter string) ([]string, error) {
	vars, err := client.ListPackageVariables(filter)
	if err != nil {
		return nil, err
	}
	return filterVariables(vars, filter), nil
}
D
Dan Mace 已提交
444

D
Derek Parker 已提交
445 446 447 448 449 450 451 452
func regs(client service.Client, args ...string) error {
	regs, err := client.ListRegisters()
	if err != nil {
		return err
	}
	fmt.Println(regs)
	return nil
}
453

D
Derek Parker 已提交
454 455 456 457 458 459 460 461
func filterSortAndOutput(fn func(client service.Client, filter string) ([]string, error)) cmdfunc {
	return func(client service.Client, 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]
D
Dan Mace 已提交
462
		}
D
Derek Parker 已提交
463
		data, err := fn(client, filter)
D
Dan Mace 已提交
464 465 466
		if err != nil {
			return err
		}
D
Derek Parker 已提交
467 468 469
		sort.Sort(sort.StringSlice(data))
		for _, d := range data {
			fmt.Println(d)
D
Dan Mace 已提交
470
		}
D
Derek Parker 已提交
471
		return nil
D
Dan Mace 已提交
472 473 474
	}
}

A
aarzilli 已提交
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
func stackCommand(client service.Client, args ...string) error {
	var err error

	goroutineid := -1
	depth := 10

	switch len(args) {
	case 0:
		// nothing to do
	case 2:
		goroutineid, err = strconv.Atoi(args[1])
		if err != nil {
			return fmt.Errorf("Wrong argument: expected integer")
		}
		fallthrough
	case 1:
		depth, err = strconv.Atoi(args[0])
		if err != nil {
			return fmt.Errorf("Wrong argument: expected integer")
		}

	default:
		return fmt.Errorf("Wrong number of arguments to stack")
	}

	stack, err := client.Stacktrace(goroutineid, depth)
	if err != nil {
		return err
	}
A
aarzilli 已提交
504 505 506 507
	printStack(stack, "")
	return nil
}

508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
func listCommand(client service.Client, args ...string) error {
	if len(args) == 0 {
		state, err := client.GetState()
		if err != nil {
			return err
		}
		printcontext(state)
		return nil
	}

	locs, err := client.FindLocation(args[0])
	if err != nil {
		return err
	}

	if len(locs) > 1 {
		return debugger.AmbiguousLocationError{Location: args[0], CandidatesLocation: locs}
	}

	printfile(locs[0].File, locs[0].Line, false)
	return nil
}

A
aarzilli 已提交
531
func printStack(stack []api.Location, ind string) {
A
aarzilli 已提交
532 533 534 535 536
	for i := range stack {
		name := "(nil)"
		if stack[i].Function != nil {
			name = stack[i].Function.Name
		}
A
aarzilli 已提交
537
		fmt.Printf("%s%d. %s %s:%d (%#v)\n", ind, i, name, stack[i].File, stack[i].Line, stack[i].PC)
A
aarzilli 已提交
538 539 540
	}
}

D
Dan Mace 已提交
541 542 543 544 545 546 547 548 549 550 551 552
func printcontext(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)
		fmt.Printf("\033[34m=>\033[0m    no source available\n")
		return nil
	}

D
Derek Parker 已提交
553
	var fn *api.Function
D
Dan Mace 已提交
554
	if state.CurrentThread.Function != nil {
D
Derek Parker 已提交
555 556 557 558 559 560 561
		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)
		}
562
		fmt.Printf("> %s(%s) %s:%d\n", fn.Name, strings.Join(args, ", "), state.CurrentThread.File, state.CurrentThread.Line)
D
Derek Parker 已提交
563
	} else {
564
		fmt.Printf("> %s() %s:%d\n", fn.Name, state.CurrentThread.File, state.CurrentThread.Line)
D
Dan Mace 已提交
565 566
	}

A
aarzilli 已提交
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589
	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
	}

590 591 592 593 594
	return printfile(state.CurrentThread.File, state.CurrentThread.Line, true)
}

func printfile(filename string, line int, showArrow bool) error {
	file, err := os.Open(filename)
D
Dan Mace 已提交
595 596 597 598 599
	if err != nil {
		return err
	}
	defer file.Close()

600 601
	var context []string

D
Dan Mace 已提交
602
	buf := bufio.NewReader(file)
603
	l := line
D
Dan Mace 已提交
604 605 606 607 608 609 610
	for i := 1; i < l-5; i++ {
		_, err := buf.ReadString('\n')
		if err != nil && err != io.EOF {
			return err
		}
	}

611 612 613 614 615 616
	s := l - 5
	if s < 1 {
		s = 1
	}

	for i := s; i <= l+5; i++ {
D
Dan Mace 已提交
617 618 619 620 621 622 623 624 625 626 627
		line, err := buf.ReadString('\n')
		if err != nil {
			if err != io.EOF {
				return err
			}

			if err == io.EOF {
				break
			}
		}

628 629 630 631 632 633
		var arrow string
		if showArrow {
			arrow = "  "
			if i == l {
				arrow = "=>"
			}
D
Dan Mace 已提交
634 635
		}

636 637 638 639 640 641 642
		var lineNum string
		if i < 10 {
			lineNum = fmt.Sprintf("\033[34m%s  %d\033[0m:\t", arrow, i)
		} else {
			lineNum = fmt.Sprintf("\033[34m%s %d\033[0m:\t", arrow, i)
		}
		context = append(context, lineNum+line)
D
Dan Mace 已提交
643 644 645 646 647 648
	}

	fmt.Println(strings.Join(context, ""))

	return nil
}
D
Derek Parker 已提交
649 650 651 652 653 654 655 656 657 658

type ExitRequestError struct{}

func (ere ExitRequestError) Error() string {
	return ""
}

func exitCommand(client service.Client, args ...string) error {
	return ExitRequestError{}
}