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:
parent
3d42ff0ad8
commit
c5c41f6352
17
_fixtures/restartargs.go
Normal file
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
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
31
vendor/github.com/cosiner/argv/README.md
generated
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Argv
|
||||
|
||||
[](https://godoc.org/github.com/cosiner/argv)
|
||||
[](https://travis-ci.org/cosiner/argv)
|
||||
[](https://coveralls.io/github/cosiner/argv)
|
||||
[](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
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
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
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
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user