command/terminal: allow restart to change process args (#1060)

* command/terminal: allow restart to change process args

Add -args flag to "restart" command. For example, "restart -args a b c" will
pass args a b c to the new process.

Add "-c" flag to pass the checkpoint name. This is needed to disambiguate the
checkpoint name and arglist.

Reverted unnecessary changes.

* Applied reviewer comments.

Vendored argv.

Change the syntax of restart. When the target is is in recording mode, it always
interprets the args as a checkpoint. Otherwise, it interprets the args as
commandline args. The flag "-args" is still there, to handle the case in which
the user wants to pass an empty args on restart.

* Add restartargs.go.

Change "restart -args" to "restart -noargs" to clarify that this flag is used to
start a process with an empty arg.
This commit is contained in:
Yasushi Saito 2018-01-18 14:16:11 -08:00 committed by Derek Parker
parent 3d42ff0ad8
commit c5c41f6352
17 changed files with 829 additions and 52 deletions

17
_fixtures/restartargs.go Normal file

@ -0,0 +1,17 @@
package main
import (
"fmt"
"os"
)
var args []string
func printArgs(){
fmt.Printf("Args2: %#v\n", args)
}
func main() {
args = os.Args[1:]
printArgs()
}

@ -572,7 +572,7 @@ func gobuild(debugname, pkg string) error {
}
func gotestbuild(debugname, pkg string) error {
args := []string{ "-c", "-o", debugname}
args := []string{"-c", "-o", debugname}
args = optflags(args)
if BuildFlags != "" {
args = append(args, config.SplitQuotedFields(BuildFlags, '\'')...)

@ -18,6 +18,7 @@ import (
"strings"
"text/tabwriter"
"github.com/cosiner/argv"
"github.com/derekparker/delve/service"
"github.com/derekparker/delve/service/api"
"github.com/derekparker/delve/service/debugger"
@ -85,7 +86,7 @@ func DebugCommands(client service.Client) *Commands {
{aliases: []string{"help", "h"}, cmdFn: c.help, helpMsg: `Prints the help message.
help [command]
Type "help" followed by the name of a command for more information about it.`},
{aliases: []string{"break", "b"}, cmdFn: breakpoint, helpMsg: `Sets a breakpoint.
@ -97,11 +98,19 @@ See also: "help on", "help cond" and "help clear"`},
{aliases: []string{"trace", "t"}, cmdFn: tracepoint, helpMsg: `Set tracepoint.
trace [name] <linespec>
A tracepoint is a breakpoint that does not stop the execution of the program, instead when the tracepoint is hit a notification is displayed. See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/locspec.md for the syntax of linespec.
See also: "help on", "help cond" and "help clear"`},
{aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: "Restart process."},
{aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: `Restart process.
restart [checkpoint]
restart [-noargs] newargv...
For recorded processes restarts from the start or from the specified
checkpoint. For normal processes restarts the process, optionally changing
the arguments. With -noargs, the process starts with an empty commandline.
`},
{aliases: []string{"continue", "c"}, cmdFn: cont, helpMsg: "Run until breakpoint or program termination."},
{aliases: []string{"step", "s"}, allowedPrefixes: scopePrefix, cmdFn: step, helpMsg: "Single step through program."},
{aliases: []string{"step-instruction", "si"}, allowedPrefixes: scopePrefix, cmdFn: stepInstruction, helpMsg: "Single step a single cpu instruction."},
@ -117,7 +126,7 @@ See also: "help on", "help cond" and "help clear"`},
{aliases: []string{"clearall"}, cmdFn: clearAll, helpMsg: `Deletes multiple breakpoints.
clearall [<linespec>]
If called with the linespec argument it will delete all the breakpoints matching the linespec. If linespec is omitted all breakpoints are deleted.`},
{aliases: []string{"goroutines"}, cmdFn: goroutines, helpMsg: `List program goroutines.
@ -128,7 +137,7 @@ Print out info for every goroutine. The flag controls what information is shown
-u displays location of topmost stackframe in user code
-r displays location of topmost stackframe (including frames inside private runtime functions)
-g displays location of go instruction that created the goroutine
If no flag is specified the default is -u.`},
{aliases: []string{"goroutine"}, allowedPrefixes: onPrefix | scopePrefix, cmdFn: c.goroutine, helpMsg: `Shows or changes current goroutine
@ -146,7 +155,7 @@ Called with more arguments it will execute a command on the specified goroutine.
See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/expr.md for a description of supported expressions.`},
{aliases: []string{"whatis"}, allowedPrefixes: scopePrefix, cmdFn: whatisCommand, helpMsg: `Prints type of an expression.
whatis <expression>.`},
{aliases: []string{"set"}, allowedPrefixes: scopePrefix, cmdFn: setVar, helpMsg: `Changes the value of a variable.
@ -188,7 +197,7 @@ If regex is specified only package variables with a name matching it will be ret
{aliases: []string{"regs"}, cmdFn: regs, helpMsg: `Print contents of CPU registers.
regs [-a]
Argument -a shows more registers.`},
{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: "Exit the debugger."},
{aliases: []string{"list", "ls"}, allowedPrefixes: scopePrefix, cmdFn: listCommand, helpMsg: `Show source code.
@ -199,7 +208,7 @@ Show source around current point or provided linespec.`},
{aliases: []string{"stack", "bt"}, allowedPrefixes: scopePrefix | onPrefix, cmdFn: stackCommand, helpMsg: `Print stack trace.
[goroutine <n>] [frame <m>] stack [<depth>] [-full] [-g] [-s] [-offsets]
-full every stackframe is decorated with the value of its local variables and arguments.
-offsets prints frame offset of each frame
`},
@ -214,23 +223,23 @@ Show source around current point or provided linespec.`},
[goroutine <n>] [frame <m>] disassemble [-a <start> <end>] [-l <locspec>]
If no argument is specified the function being executed in the selected stack frame will be executed.
-a <start> <end> disassembles the specified address range
-l <locspec> disassembles the specified function`},
{aliases: []string{"on"}, cmdFn: c.onCmd, helpMsg: `Executes a command when a breakpoint is hit.
on <breakpoint name or id> <command>.
Supported commands: print, stack and goroutine)`},
{aliases: []string{"condition", "cond"}, cmdFn: conditionCmd, helpMsg: `Set breakpoint condition.
condition <breakpoint name or id> <boolean expression>.
Specifies that the breakpoint or tracepoint should break only if the boolean expression is true.`},
{aliases: []string{"config"}, cmdFn: configureCmd, helpMsg: `Changes configuration parameters.
config -list
Show all configuration parameters.
config -save
@ -238,17 +247,17 @@ Show all configuration parameters.
Saves the configuration file to disk, overwriting the current configuration file.
config <parameter> <value>
Changes the value of a configuration parameter.
config subistitute-path <from> <to>
config subistitute-path <from>
Adds or removes a path subistitution rule.
config alias <command> <alias>
config alias <alias>
Defines <alias> as an alias to <command> or removes an alias.`},
}
@ -262,7 +271,7 @@ Defines <alias> as an alias to <command> or removes an alias.`},
aliases: []string{"check", "checkpoint"},
cmdFn: checkpoint,
helpMsg: `Creates a checkpoint at the current position.
checkpoint [where]`,
})
c.cmds = append(c.cmds, command{
@ -274,15 +283,15 @@ Defines <alias> as an alias to <command> or removes an alias.`},
aliases: []string{"clear-checkpoint", "clearcheck"},
cmdFn: clearCheckpoint,
helpMsg: `Deletes checkpoint.
clear-checkpoint <id>`,
})
for i := range c.cmds {
v := &c.cmds[i]
if v.match("restart") {
v.helpMsg = `Restart process from a checkpoint or event.
restart [event number or checkpoint id]`
restart [event number or checkpoint id]`
}
}
}
@ -653,8 +662,48 @@ func writeGoroutineLong(w io.Writer, g *api.Goroutine, prefix string) {
prefix, formatLocation(g.GoStatementLoc))
}
func parseArgs(args string) ([]string, error) {
if args == "" {
return nil, nil
}
v, err := argv.Argv([]rune(args), argv.ParseEnv(os.Environ()),
func(s []rune, _ map[string]string) ([]rune, error) {
return nil, fmt.Errorf("Backtick not supported in '%s'", string(s))
})
if err != nil {
return nil, err
}
if len(v) != 1 {
return nil, fmt.Errorf("Illegal commandline '%s'", args)
}
return v[0], nil
}
func restart(t *Term, ctx callContext, args string) error {
discarded, err := t.client.RestartFrom(args)
v, err := parseArgs(args)
if err != nil {
return err
}
var restartPos string
var resetArgs bool
if t.client.Recorded() {
if len(v) > 1 {
return fmt.Errorf("restart: illegal position '%v'", v)
}
if len(v) == 1 {
restartPos = v[0]
v = nil
}
} else if len(v) > 0 {
resetArgs = true
if v[0] == "-noargs" {
if len(v) > 1 {
return fmt.Errorf("restart: -noargs does not take any arg")
}
v = nil
}
}
discarded, err := t.client.RestartFrom(restartPos, resetArgs, v)
if err != nil {
return err
}

@ -591,6 +591,34 @@ func TestCheckpoints(t *testing.T) {
})
}
func TestRestart(t *testing.T) {
withTestTerminal("restartargs", t, func(term *FakeTerminal) {
term.MustExec("break main.printArgs")
term.MustExec("continue")
if out := term.MustExec("print main.args"); !strings.Contains(out, ", []") {
t.Fatalf("wrong args: %q", out)
}
// Reset the arg list
term.MustExec("restart hello")
term.MustExec("continue")
if out := term.MustExec("print main.args"); !strings.Contains(out, ", [\"hello\"]") {
t.Fatalf("wrong args: %q ", out)
}
// Restart w/o arg should retain the current args.
term.MustExec("restart")
term.MustExec("continue")
if out := term.MustExec("print main.args"); !strings.Contains(out, ", [\"hello\"]") {
t.Fatalf("wrong args: %q ", out)
}
// Empty arg list
term.MustExec("restart -noargs")
term.MustExec("continue")
if out := term.MustExec("print main.args"); !strings.Contains(out, ", []") {
t.Fatalf("wrong args: %q ", out)
}
})
}
func TestIssue827(t *testing.T) {
// switching goroutines when the current thread isn't running any goroutine
// causes nil pointer dereference.

@ -21,7 +21,7 @@ type Client interface {
// Restarts program.
Restart() ([]api.DiscardedBreakpoint, error)
// Restarts program from the specified position.
RestartFrom(pos string) ([]api.DiscardedBreakpoint, error)
RestartFrom(pos string, resetArgs bool, newArgs []string) ([]api.DiscardedBreakpoint, error)
// GetState returns the current debugger state.
GetState() (*api.DebuggerState, error)

@ -29,6 +29,8 @@ import (
// lower lever packages such as proc.
type Debugger struct {
config *Config
// arguments to launch a new process.
processArgs []string
// TODO(DO NOT MERGE WITHOUT) rename to targetMutex
processMutex sync.Mutex
target proc.Process
@ -40,8 +42,6 @@ type Debugger struct {
// provided, a new process will be launched. Otherwise, the debugger will try
// to attach to an existing process with AttachPid.
type Config struct {
// ProcessArgs are the arguments to launch a new process.
ProcessArgs []string
// WorkingDir is working directory of the new process. This field is used
// only when launching a new process.
WorkingDir string
@ -56,10 +56,12 @@ type Config struct {
Backend string
}
// New creates a new Debugger.
func New(config *Config) (*Debugger, error) {
// New creates a new Debugger. ProcessArgs specify the commandline arguments for the
// new process.
func New(config *Config, processArgs []string) (*Debugger, error) {
d := &Debugger{
config: config,
config: config,
processArgs: processArgs,
}
// Create the process by either attaching or launching.
@ -67,8 +69,8 @@ func New(config *Config) (*Debugger, error) {
case d.config.AttachPid > 0:
log.Printf("attaching to pid %d", d.config.AttachPid)
path := ""
if len(d.config.ProcessArgs) > 0 {
path = d.config.ProcessArgs[0]
if len(d.processArgs) > 0 {
path = d.processArgs[0]
}
p, err := d.Attach(d.config.AttachPid, path)
if err != nil {
@ -84,8 +86,8 @@ func New(config *Config) (*Debugger, error) {
log.Printf("opening trace %s", d.config.CoreFile)
p, err = gdbserial.Replay(d.config.CoreFile, false)
default:
log.Printf("opening core file %s (executable %s)", d.config.CoreFile, d.config.ProcessArgs[0])
p, err = core.OpenCore(d.config.CoreFile, d.config.ProcessArgs[0])
log.Printf("opening core file %s (executable %s)", d.config.CoreFile, d.processArgs[0])
p, err = core.OpenCore(d.config.CoreFile, d.processArgs[0])
}
if err != nil {
return nil, err
@ -93,8 +95,8 @@ func New(config *Config) (*Debugger, error) {
d.target = p
default:
log.Printf("launching process with args: %v", d.config.ProcessArgs)
p, err := d.Launch(d.config.ProcessArgs, d.config.WorkingDir)
log.Printf("launching process with args: %v", d.processArgs)
p, err := d.Launch(d.processArgs, d.config.WorkingDir)
if err != nil {
if err != proc.NotExecutableErr && err != proc.UnsupportedLinuxArchErr && err != proc.UnsupportedWindowsArchErr && err != proc.UnsupportedDarwinArchErr {
err = fmt.Errorf("could not launch process: %s", err)
@ -179,8 +181,8 @@ func (d *Debugger) detach(kill bool) error {
// and then exec'ing it again.
// If the target process is a recording it will restart it from the given
// position. If pos starts with 'c' it's a checkpoint ID, otherwise it's an
// event number.
func (d *Debugger) Restart(pos string) ([]api.DiscardedBreakpoint, error) {
// event number. If resetArgs is true, newArgs will replace the process args.
func (d *Debugger) Restart(pos string, resetArgs bool, newArgs []string) ([]api.DiscardedBreakpoint, error) {
d.processMutex.Lock()
defer d.processMutex.Unlock()
@ -201,7 +203,10 @@ func (d *Debugger) Restart(pos string) ([]api.DiscardedBreakpoint, error) {
if err := d.detach(true); err != nil {
return nil, err
}
p, err := d.Launch(d.config.ProcessArgs, d.config.WorkingDir)
if resetArgs {
d.processArgs = append([]string{d.processArgs[0]}, newArgs...)
}
p, err := d.Launch(d.processArgs, d.config.WorkingDir)
if err != nil {
return nil, fmt.Errorf("could not launch process: %s", err)
}

@ -14,10 +14,10 @@ import (
// Client is a RPC service.Client.
type RPCClient struct {
addr string
client *rpc.Client
haltMu sync.Mutex
haltReq bool
addr string
client *rpc.Client
haltMu sync.Mutex
haltReq bool
}
var unsupportedApiError = errors.New("unsupported")

@ -36,7 +36,7 @@ func (s *RPCServer) Restart(arg1 interface{}, arg2 *int) error {
if s.config.AttachPid != 0 {
return errors.New("cannot restart process Delve did not create")
}
_, err := s.debugger.Restart("")
_, err := s.debugger.Restart("", false, nil)
return err
}

@ -51,13 +51,13 @@ func (c *RPCClient) Detach(kill bool) error {
func (c *RPCClient) Restart() ([]api.DiscardedBreakpoint, error) {
out := new(RestartOut)
err := c.call("Restart", RestartIn{""}, out)
err := c.call("Restart", RestartIn{"", false, nil}, out)
return out.DiscardedBreakpoints, err
}
func (c *RPCClient) RestartFrom(pos string) ([]api.DiscardedBreakpoint, error) {
func (c *RPCClient) RestartFrom(pos string, resetArgs bool, newArgs []string) ([]api.DiscardedBreakpoint, error) {
out := new(RestartOut)
err := c.call("Restart", RestartIn{pos}, out)
err := c.call("Restart", RestartIn{pos, resetArgs, newArgs}, out)
return out.DiscardedBreakpoints, err
}

@ -62,6 +62,12 @@ type RestartIn struct {
// Position to restart from, if it starts with 'c' it's a checkpoint ID,
// otherwise it's an event number. Only valid for recorded targets.
Position string
// ResetArgs tell whether NewArgs should take effect.
ResetArgs bool
// NewArgs are arguments to launch a new process. They replace only the
// argv[1] and later. Argv[0] cannot be changed.
NewArgs []string
}
type RestartOut struct {
@ -74,7 +80,7 @@ func (s *RPCServer) Restart(arg RestartIn, out *RestartOut) error {
return errors.New("cannot restart process Delve did not create")
}
var err error
out.DiscardedBreakpoints, err = s.debugger.Restart(arg.Position)
out.DiscardedBreakpoints, err = s.debugger.Restart(arg.Position, arg.ResetArgs, arg.NewArgs)
return err
}

@ -111,12 +111,12 @@ func (s *ServerImpl) Run() error {
// Create and start the debugger
if s.debugger, err = debugger.New(&debugger.Config{
ProcessArgs: s.config.ProcessArgs,
AttachPid: s.config.AttachPid,
WorkingDir: s.config.WorkingDir,
CoreFile: s.config.CoreFile,
Backend: s.config.Backend,
}); err != nil {
AttachPid: s.config.AttachPid,
WorkingDir: s.config.WorkingDir,
CoreFile: s.config.CoreFile,
Backend: s.config.Backend,
},
s.config.ProcessArgs); err != nil {
return err
}

24
vendor/github.com/cosiner/argv/LICENSE generated vendored Normal file

@ -0,0 +1,24 @@
The MIT License (MIT)
Copyright (c) 2017 aihui zhu
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

31
vendor/github.com/cosiner/argv/README.md generated vendored Normal file

@ -0,0 +1,31 @@
# Argv
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/cosiner/argv)
[![Build Status](https://travis-ci.org/cosiner/argv.svg?branch=master&style=flat)](https://travis-ci.org/cosiner/argv)
[![Coverage Status](https://coveralls.io/repos/github/cosiner/argv/badge.svg?style=flat)](https://coveralls.io/github/cosiner/argv)
[![Go Report Card](https://goreportcard.com/badge/github.com/cosiner/argv?style=flat)](https://goreportcard.com/report/github.com/cosiner/argv)
Argv is a library for [Go](https://golang.org) to split command line string into arguments array.
# Documentation
Documentation can be found at [Godoc](https://godoc.org/github.com/cosiner/argv)
# Example
```Go
func TestArgv(t *testing.T) {
args, err := argv.Argv([]rune(" ls `echo /` | wc -l "), os.Environ(), argv.Run)
if err != nil {
t.Fatal(err)
}
expects := [][]string{
[]string{"ls", "/"},
[]string{"wc", "-l"},
}
if !reflect.DeepDqual(args, expects) {
t.Fatal(args)
}
}
```
# LICENSE
MIT.

34
vendor/github.com/cosiner/argv/argv.go generated vendored Normal file

@ -0,0 +1,34 @@
// Package argv parse command line string into arguments array using the bash syntax.
package argv
import "strings"
// ParseEnv parsing environment variables as key/value pair.
//
// Item will be ignored if one of the key and value is empty.
func ParseEnv(env []string) map[string]string {
var m map[string]string
for _, e := range env {
secs := strings.SplitN(e, "=", 2)
if len(secs) == 2 {
key := strings.TrimSpace(secs[0])
val := strings.TrimSpace(secs[1])
if key == "" || val == "" {
continue
}
if m == nil {
m = make(map[string]string)
}
m[key] = val
}
}
return m
}
// Argv split cmdline string as array of argument array by the '|' character.
//
// The parsing rules is same as bash. The environment variable will be replaced
// and string surround by '`' will be passed to reverse quote parser.
func Argv(cmdline []rune, env map[string]string, reverseQuoteParser ReverseQuoteParser) ([][]string, error) {
return NewParser(NewScanner(cmdline, env), reverseQuoteParser).Parse()
}

79
vendor/github.com/cosiner/argv/cmd.go generated vendored Normal file

@ -0,0 +1,79 @@
package argv
import (
"bytes"
"errors"
"io"
"os/exec"
"strings"
)
// Run execute cmdline string and return the output
func Run(cmdline []rune, env map[string]string) ([]rune, error) {
args, err := Argv(cmdline, env, Run)
if err != nil {
return nil, err
}
cmds, err := Cmds(args)
if err != nil {
return nil, err
}
output := bytes.NewBuffer(make([]byte, 0, 1024))
err = Pipe(nil, output, cmds...)
str := output.String()
str = strings.TrimSpace(str)
return []rune(str), err
}
// Cmds generate exec.Cmd for each command.
func Cmds(args [][]string) ([]*exec.Cmd, error) {
var cmds []*exec.Cmd
for _, argv := range args {
if len(argv) == 0 {
return nil, errors.New("invalid cmd")
}
cmds = append(cmds, exec.Command(argv[0], argv[1:]...))
}
return cmds, nil
}
// Pipe pipe previous command's stdout to next command's stdin, if in or
// out is nil, it will be ignored.
func Pipe(in io.Reader, out io.Writer, cmds ...*exec.Cmd) error {
l := len(cmds)
if l == 0 {
return nil
}
var err error
for i := 1; i < l; i++ {
cmds[i].Stdin, err = cmds[i-1].StdoutPipe()
if err != nil {
break
}
}
if err != nil {
return err
}
if in != nil {
cmds[0].Stdin = in
}
if out != nil {
cmds[l-1].Stdout = out
}
for i := range cmds {
err = cmds[i].Start()
if err != nil {
return err
}
}
for i := range cmds {
err = cmds[i].Wait()
if err != nil {
return err
}
}
return nil
}

222
vendor/github.com/cosiner/argv/parser.go generated vendored Normal file

@ -0,0 +1,222 @@
package argv
import "errors"
type (
// ReverseQuoteParser parse strings quoted by '`' and return it's result. Commonly,
// it should run it os command.
ReverseQuoteParser func([]rune, map[string]string) ([]rune, error)
// Parser take tokens from Scanner, and do syntax checking, and generate the splitted arguments array.
Parser struct {
s *Scanner
tokbuf []Token
r ReverseQuoteParser
sections [][]string
currSection []string
currStrValid bool
currStr []rune
}
)
// NewParser create a cmdline string parser.
func NewParser(s *Scanner, r ReverseQuoteParser) *Parser {
if r == nil {
r = func(r []rune, env map[string]string) ([]rune, error) {
return r, nil
}
}
return &Parser{
s: s,
r: r,
}
}
func (p *Parser) nextToken() (Token, error) {
if l := len(p.tokbuf); l > 0 {
tok := p.tokbuf[l-1]
p.tokbuf = p.tokbuf[:l-1]
return tok, nil
}
return p.s.Next()
}
var (
// ErrInvalidSyntax was reported if there is a syntax error in command line string.
ErrInvalidSyntax = errors.New("invalid syntax")
)
func (p *Parser) unreadToken(tok Token) {
p.tokbuf = append(p.tokbuf, tok)
}
// Parse split command line string into arguments array.
//
// EBNF:
// Cmdline = Section [ Pipe Cmdline ]
// Section = [Space] SpacedSection { SpacedSection }
// SpacedSection = MultipleUnit [Space]
// MultipleUnit = Unit {Unit}
// Unit = String | ReverseQuote
func (p *Parser) Parse() ([][]string, error) {
err := p.cmdline()
if err != nil {
return nil, err
}
return p.sections, nil
}
func (p *Parser) cmdline() error {
err := p.section()
if err != nil {
return err
}
p.endSection()
tok, err := p.nextToken()
if err != nil {
return err
}
if tok.Type == TokEOF {
return nil
}
if !p.accept(tok.Type, TokPipe) {
return ErrInvalidSyntax
}
return p.cmdline()
}
func (p *Parser) section() error {
leftSpace, err := p.optional(TokSpace)
if err != nil {
return err
}
var isFirst = true
for {
unit, err := p.spacedSection()
if isFirst {
isFirst = false
} else {
if err == ErrInvalidSyntax {
break
}
}
if err != nil {
return err
}
p.appendUnit(leftSpace, unit)
leftSpace = unit.rightSpace
}
return nil
}
type unit struct {
rightSpace bool
toks []Token
}
func (p *Parser) spacedSection() (u unit, err error) {
u.toks, err = p.multipleUnit()
if err != nil {
return
}
u.rightSpace, err = p.optional(TokSpace)
return
}
func (p *Parser) multipleUnit() ([]Token, error) {
var (
toks []Token
isFirst = true
)
for {
tok, err := p.unit()
if isFirst {
isFirst = false
} else {
if err == ErrInvalidSyntax {
break
}
}
if err != nil {
return nil, err
}
toks = append(toks, tok)
}
return toks, nil
}
func (p *Parser) unit() (Token, error) {
tok, err := p.nextToken()
if err != nil {
return tok, err
}
if p.accept(tok.Type, TokString, TokReversequote) {
return tok, nil
}
p.unreadToken(tok)
return tok, ErrInvalidSyntax
}
func (p *Parser) optional(typ TokenType) (bool, error) {
tok, err := p.nextToken()
if err != nil {
return false, err
}
var ok bool
if ok = p.accept(tok.Type, typ); !ok {
p.unreadToken(tok)
}
return ok, nil
}
func (p *Parser) accept(t TokenType, types ...TokenType) bool {
for _, typ := range types {
if t == typ {
return true
}
}
return false
}
func (p *Parser) appendUnit(leftSpace bool, u unit) error {
if leftSpace {
p.currStr = p.currStr[:0]
}
for _, tok := range u.toks {
if tok.Type == TokReversequote {
val, err := p.r(tok.Value, p.s.envs())
if err != nil {
return err
}
p.currStr = append(p.currStr, val...)
} else {
p.currStr = append(p.currStr, tok.Value...)
}
}
p.currStrValid = true
if u.rightSpace {
p.currSection = append(p.currSection, string(p.currStr))
p.currStr = p.currStr[:0]
p.currStrValid = false
}
return nil
}
func (p *Parser) endSection() {
if p.currStrValid {
p.currSection = append(p.currSection, string(p.currStr))
}
p.currStr = p.currStr[:0]
p.currStrValid = false
if len(p.currSection) > 0 {
p.sections = append(p.sections, p.currSection)
p.currSection = nil
}
}

282
vendor/github.com/cosiner/argv/scanner.go generated vendored Normal file

@ -0,0 +1,282 @@
package argv
import "unicode"
// Scanner is a cmdline string scanner.
//
// It split cmdline string to tokens: space, string, pipe, reverse quote string.
type Scanner struct {
env map[string]string
text []rune
rpos int
dollarBuf []rune
}
// NewScanner create a scanner and init it's internal states.
func NewScanner(text []rune, env map[string]string) *Scanner {
return &Scanner{
text: text,
env: env,
}
}
func (s *Scanner) envs() map[string]string {
return s.env
}
const _RuneEOF = 0
func (s *Scanner) nextRune() rune {
if s.rpos >= len(s.text) {
return _RuneEOF
}
r := s.text[s.rpos]
s.rpos++
return r
}
func (s *Scanner) unreadRune(r rune) {
if r != _RuneEOF {
s.rpos--
}
}
func (s *Scanner) isEscapeChars(r rune) (rune, bool) {
switch r {
case 'a':
return '\a', true
case 'b':
return '\b', true
case 'f':
return '\f', true
case 'n':
return '\n', true
case 'r':
return '\r', true
case 't':
return '\t', true
case 'v':
return '\v', true
case '\\':
return '\\', true
case '$':
return '$', true
}
return r, false
}
func (s *Scanner) endEnv(r rune) bool {
if r == '_' || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
return false
}
return true
}
// TokenType is the type of tokens recognized by the scanner.
type TokenType uint32
// Token is generated by the scanner with a type and value.
type Token struct {
Type TokenType
Value []rune
}
const (
// TokString for string, single quoted string and double quoted string
TokString TokenType = iota + 1
// TokPipe is the '|' character
TokPipe
// TokReversequote is reverse quoted string
TokReversequote
// TokSpace represent space character sequence
TokSpace
// TokEOF means the input end.
TokEOF
)
func (s *Scanner) getEnv(name string) string {
return s.env[name]
}
func (s *Scanner) specialVar(r rune) (string, bool) {
switch r {
case '0', '*', '#', '@', '?', '$':
v, has := s.env[string(r)]
return v, has
default:
return "", false
}
}
func (s *Scanner) checkDollarStart(tok *Token, r rune, from, switchTo uint8) uint8 {
state := from
nr := s.nextRune()
if val, has := s.specialVar(nr); has {
if val != "" {
tok.Value = append(tok.Value, []rune(val)...)
}
} else if s.endEnv(nr) {
tok.Value = append(tok.Value, r)
s.unreadRune(nr)
} else {
state = switchTo
s.dollarBuf = append(s.dollarBuf[:0], nr)
}
return state
}
func (s *Scanner) checkDollarEnd(tok *Token, r rune, from, switchTo uint8) uint8 {
var state = from
if s.endEnv(r) {
tok.Value = append(tok.Value, []rune(s.getEnv(string(s.dollarBuf)))...)
state = switchTo
s.unreadRune(r)
} else {
s.dollarBuf = append(s.dollarBuf, r)
}
return state
}
// Next return next token, if it reach the end, TOK_EOF will be returned.
//
// Error is returned for invalid syntax such as unpaired quotes.
func (s *Scanner) Next() (Token, error) {
const (
Initial = iota + 1
Space
ReverseQuote
String
StringDollar
StringQuoteSingle
StringQuoteDouble
StringQuoteDoubleDollar
)
var (
tok Token
state uint8 = Initial
)
s.dollarBuf = s.dollarBuf[:0]
for {
r := s.nextRune()
switch state {
case Initial:
switch {
case r == _RuneEOF:
tok.Type = TokEOF
return tok, nil
case r == '|':
tok.Type = TokPipe
return tok, nil
case r == '`':
state = ReverseQuote
case unicode.IsSpace(r):
state = Space
s.unreadRune(r)
default:
state = String
s.unreadRune(r)
}
case Space:
if r == _RuneEOF || !unicode.IsSpace(r) {
s.unreadRune(r)
tok.Type = TokSpace
return tok, nil
}
case ReverseQuote:
switch r {
case _RuneEOF:
return tok, ErrInvalidSyntax
case '`':
tok.Type = TokReversequote
return tok, nil
default:
tok.Value = append(tok.Value, r)
}
case String:
switch {
case r == _RuneEOF || r == '|' || r == '`' || unicode.IsSpace(r):
tok.Type = TokString
s.unreadRune(r)
return tok, nil
case r == '\'':
state = StringQuoteSingle
case r == '"':
state = StringQuoteDouble
case r == '\\':
nr := s.nextRune()
if nr == _RuneEOF {
return tok, ErrInvalidSyntax
}
tok.Value = append(tok.Value, nr)
case r == '$':
state = s.checkDollarStart(&tok, r, state, StringDollar)
default:
tok.Value = append(tok.Value, r)
}
case StringDollar:
state = s.checkDollarEnd(&tok, r, state, String)
case StringQuoteSingle:
switch r {
case _RuneEOF:
return tok, ErrInvalidSyntax
case '\'':
state = String
case '\\':
nr := s.nextRune()
if escape, ok := s.isEscapeChars(nr); ok {
tok.Value = append(tok.Value, escape)
} else {
tok.Value = append(tok.Value, r)
s.unreadRune(nr)
}
default:
tok.Value = append(tok.Value, r)
}
case StringQuoteDouble:
switch r {
case _RuneEOF:
return tok, ErrInvalidSyntax
case '"':
state = String
case '\\':
nr := s.nextRune()
if nr == _RuneEOF {
return tok, ErrInvalidSyntax
}
if escape, ok := s.isEscapeChars(nr); ok {
tok.Value = append(tok.Value, escape)
} else {
tok.Value = append(tok.Value, r)
s.unreadRune(nr)
}
case '$':
state = s.checkDollarStart(&tok, r, state, StringQuoteDoubleDollar)
default:
tok.Value = append(tok.Value, r)
}
case StringQuoteDoubleDollar:
state = s.checkDollarEnd(&tok, r, state, StringQuoteDouble)
}
}
}
// Scan is a utility function help split input text as tokens.
func Scan(text []rune, env map[string]string) ([]Token, error) {
s := NewScanner(text, env)
var tokens []Token
for {
tok, err := s.Next()
if err != nil {
return nil, err
}
tokens = append(tokens, tok)
if tok.Type == TokEOF {
break
}
}
return tokens, nil
}