terminal: add config command

Fixes #927, #644
This commit is contained in:
aarzilli 2017-07-29 06:11:06 +02:00 committed by Derek Parker
parent 55e44c9dc0
commit 48288edd18
10 changed files with 477 additions and 92 deletions

@ -11,6 +11,7 @@ Command | Description
[clear-checkpoint](#clear-checkpoint) | Deletes checkpoint.
[clearall](#clearall) | Deletes multiple breakpoints.
[condition](#condition) | Set breakpoint condition.
[config](#config) | Changes configuration parameters.
[continue](#continue) | Run until breakpoint or program termination.
[disassemble](#disassemble) | Disassembler.
[exit](#exit) | Exit the debugger.
@ -106,6 +107,32 @@ Specifies that the breakpoint or tracepoint should break only if the boolean exp
Aliases: cond
## config
Changes configuration parameters.
config -list
Show all configuration parameters.
config -save
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.
## continue
Run until breakpoint or program termination.

@ -1,7 +1,6 @@
package cmds
import (
"bytes"
"errors"
"fmt"
"net"
@ -12,7 +11,6 @@ import (
"runtime"
"strconv"
"syscall"
"unicode"
"github.com/derekparker/delve/pkg/config"
"github.com/derekparker/delve/pkg/goversion"
@ -534,7 +532,7 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
func gobuild(debugname, pkg string) error {
args := []string{"-gcflags", "-N -l", "-o", debugname}
if BuildFlags != "" {
args = append(args, splitQuotedFields(BuildFlags)...)
args = append(args, config.SplitQuotedFields(BuildFlags, '\'')...)
}
if ver, _ := goversion.Installed(); ver.Major < 0 || ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}) {
// after go1.9 building with -gcflags='-N -l' and -a simultaneously works
@ -547,7 +545,7 @@ func gobuild(debugname, pkg string) error {
func gotestbuild(pkg string) error {
args := []string{"-gcflags", "-N -l", "-c", "-o", testdebugname}
if BuildFlags != "" {
args = append(args, splitQuotedFields(BuildFlags)...)
args = append(args, config.SplitQuotedFields(BuildFlags, '\'')...)
}
if ver, _ := goversion.Installed(); ver.Major < 0 || ver.AfterOrEqual(goversion.GoVersion{1, 9, -1, 0, 0, ""}) {
// after go1.9 building with -gcflags='-N -l' and -a simultaneously works
@ -565,63 +563,6 @@ func gocommand(command string, args ...string) error {
return goBuild.Run()
}
// Like strings.Fields but ignores spaces inside areas surrounded
// by single quotes.
// To specify a single quote use backslash to escape it: '\''
func splitQuotedFields(in string) []string {
type stateEnum int
const (
inSpace stateEnum = iota
inField
inQuote
inQuoteEscaped
)
state := inSpace
r := []string{}
var buf bytes.Buffer
for _, ch := range in {
switch state {
case inSpace:
if ch == '\'' {
state = inQuote
} else if !unicode.IsSpace(ch) {
buf.WriteRune(ch)
state = inField
}
case inField:
if ch == '\'' {
state = inQuote
} else if unicode.IsSpace(ch) {
r = append(r, buf.String())
buf.Reset()
} else {
buf.WriteRune(ch)
}
case inQuote:
if ch == '\'' {
state = inField
} else if ch == '\\' {
state = inQuoteEscaped
} else {
buf.WriteRune(ch)
}
case inQuoteEscaped:
buf.WriteRune(ch)
state = inQuote
}
}
if buf.Len() != 0 {
r = append(r, buf.String())
}
return r
}
// SafeRemoveAll removes dir and its contents but only as long as dir does
// not contain directories.
func SafeRemoveAll(dir string) {

@ -1,21 +0,0 @@
package cmds
import (
"testing"
)
func TestSplitQuotedFields(t *testing.T) {
in := `field'A' 'fieldB' fie'l\'d'C fieldD 'another field' fieldE`
tgt := []string{"fieldA", "fieldB", "fiel'dC", "fieldD", "another field", "fieldE"}
out := splitQuotedFields(in)
if len(tgt) != len(out) {
t.Fatalf("expected %#v, got %#v (len mismatch)", tgt, out)
}
for i := range tgt {
if tgt[i] != out[i] {
t.Fatalf(" expected %#v, got %#v (mismatch at %d)", tgt, out, i)
}
}
}

@ -20,7 +20,7 @@ type SubstitutePathRule struct {
// Directory path will be substituted if it matches `From`.
From string
// Path to which substitution is performed.
To string
To string
}
// Slice of source code path substitution rules.
@ -29,9 +29,16 @@ type SubstitutePathRules []SubstitutePathRule
// Config defines all configuration options available to be set through the config file.
type Config struct {
// Commands aliases.
Aliases map[string][]string
Aliases map[string][]string `yaml:"aliases"`
// Source code path substitution rules.
SubstitutePath SubstitutePathRules `yaml:"substitute-path"`
// MaxStringLen is the maximum string length that the commands print,
// locals, args and vars should read (in verbose mode).
MaxStringLen *int `yaml:"max-string-len,omitempty"`
// MaxArrayValues is the maximum number of array items that the commands
// print, locals, args and vars should read (in verbose mode).
MaxArrayValues *int `yaml:"max-array-values,omitempty"`
}
// LoadConfig attempts to populate a Config object from the config.yml file.
@ -75,6 +82,27 @@ func LoadConfig() *Config {
return &c
}
func SaveConfig(conf *Config) error {
fullConfigFile, err := GetConfigFilePath(configFile)
if err != nil {
return err
}
out, err := yaml.Marshal(*conf)
if err != nil {
return err
}
f, err := os.Create(fullConfigFile)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(out)
return err
}
func createDefaultConfig(path string) {
f, err := os.Create(path)
if err != nil {

63
pkg/config/split.go Normal file

@ -0,0 +1,63 @@
package config
import (
"bytes"
"unicode"
)
// Like strings.Fields but ignores spaces inside areas surrounded
// by the specified quote character.
// To specify a single quote use backslash to escape it: '\''
func SplitQuotedFields(in string, quote rune) []string {
type stateEnum int
const (
inSpace stateEnum = iota
inField
inQuote
inQuoteEscaped
)
state := inSpace
r := []string{}
var buf bytes.Buffer
for _, ch := range in {
switch state {
case inSpace:
if ch == quote {
state = inQuote
} else if !unicode.IsSpace(ch) {
buf.WriteRune(ch)
state = inField
}
case inField:
if ch == quote {
state = inQuote
} else if unicode.IsSpace(ch) {
r = append(r, buf.String())
buf.Reset()
} else {
buf.WriteRune(ch)
}
case inQuote:
if ch == quote {
state = inField
} else if ch == '\\' {
state = inQuoteEscaped
} else {
buf.WriteRune(ch)
}
case inQuoteEscaped:
buf.WriteRune(ch)
state = inQuote
}
}
if buf.Len() != 0 {
r = append(r, buf.String())
}
return r
}

37
pkg/config/split_test.go Normal file

@ -0,0 +1,37 @@
package config
import (
"testing"
)
func TestSplitQuotedFields(t *testing.T) {
in := `field'A' 'fieldB' fie'l\'d'C fieldD 'another field' fieldE`
tgt := []string{"fieldA", "fieldB", "fiel'dC", "fieldD", "another field", "fieldE"}
out := SplitQuotedFields(in, '\'')
if len(tgt) != len(out) {
t.Fatalf("expected %#v, got %#v (len mismatch)", tgt, out)
}
for i := range tgt {
if tgt[i] != out[i] {
t.Fatalf(" expected %#v, got %#v (mismatch at %d)", tgt, out, i)
}
}
}
func TestSplitDoubleQuotedFields(t *testing.T) {
in := `field"A" "fieldB" fie"l'd"C "field\"D" "yet another field"`
tgt := []string{"fieldA", "fieldB", "fiel'dC", "field\"D", "yet another field"}
out := SplitQuotedFields(in, '"')
if len(tgt) != len(out) {
t.Fatalf("expected %#v, got %#v (len mismatch)", tgt, out)
}
for i := range tgt {
if tgt[i] != out[i] {
t.Fatalf(" expected %#v, got %#v (mismatch at %d)", tgt, out, i)
}
}
}

@ -41,6 +41,7 @@ type cmdfunc func(t *Term, ctx callContext, args string) error
type command struct {
aliases []string
builtinAliases []string
allowedPrefixes cmdPrefix
helpMsg string
cmdFn cmdfunc
@ -222,6 +223,29 @@ Supported commands: print, stack and goroutine)`},
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
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.`},
}
if client == nil || client.Recorded() {
@ -318,8 +342,17 @@ func (c *Commands) Call(cmdstr string, t *Term) error {
// 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 c.cmds[i].builtinAliases != nil {
c.cmds[i].aliases = append(c.cmds[i].aliases[:0], c.cmds[i].builtinAliases...)
}
}
for i := range c.cmds {
if aliases, ok := allAliases[c.cmds[i].aliases[0]]; ok {
if c.cmds[i].builtinAliases == nil {
c.cmds[i].builtinAliases = make([]string, len(c.cmds[i].aliases))
copy(c.cmds[i].builtinAliases, c.cmds[i].aliases)
}
c.cmds[i].aliases = append(c.cmds[i].aliases, aliases...)
}
}
@ -906,7 +939,7 @@ func printVar(t *Term, ctx callContext, args string) error {
ctx.Breakpoint.Variables = append(ctx.Breakpoint.Variables, args)
return nil
}
val, err := t.client.EvalVariable(ctx.Scope, args, LongLoadConfig)
val, err := t.client.EvalVariable(ctx.Scope, args, t.loadConfig())
if err != nil {
return err
}
@ -1001,19 +1034,19 @@ func types(t *Term, ctx callContext, args string) error {
return printSortedStrings(t.client.ListTypes(args))
}
func parseVarArguments(args string) (filter string, cfg api.LoadConfig) {
func parseVarArguments(args string, t *Term) (filter string, cfg api.LoadConfig) {
if v := strings.SplitN(args, " ", 2); len(v) >= 1 && v[0] == "-v" {
if len(v) == 2 {
return v[1], LongLoadConfig
return v[1], t.loadConfig()
} else {
return "", LongLoadConfig
return "", t.loadConfig()
}
}
return args, ShortLoadConfig
}
func args(t *Term, ctx callContext, args string) error {
filter, cfg := parseVarArguments(args)
filter, cfg := parseVarArguments(args, t)
if ctx.Prefix == onPrefix {
if filter != "" {
return fmt.Errorf("filter not supported on breakpoint")
@ -1029,7 +1062,7 @@ func args(t *Term, ctx callContext, args string) error {
}
func locals(t *Term, ctx callContext, args string) error {
filter, cfg := parseVarArguments(args)
filter, cfg := parseVarArguments(args, t)
if ctx.Prefix == onPrefix {
if filter != "" {
return fmt.Errorf("filter not supported on breakpoint")
@ -1045,7 +1078,7 @@ func locals(t *Term, ctx callContext, args string) error {
}
func vars(t *Term, ctx callContext, args string) error {
filter, cfg := parseVarArguments(args)
filter, cfg := parseVarArguments(args, t)
vars, err := t.client.ListPackageVariables(filter, cfg)
if err != nil {
return err

@ -14,6 +14,7 @@ import (
"testing"
"time"
"github.com/derekparker/delve/pkg/config"
"github.com/derekparker/delve/pkg/proc/test"
"github.com/derekparker/delve/service"
"github.com/derekparker/delve/service/api"
@ -121,7 +122,7 @@ func withTestTerminal(name string, t testing.TB, fn func(*FakeTerminal)) {
ft := &FakeTerminal{
t: t,
Term: New(client, nil),
Term: New(client, &config.Config{}),
}
fn(ft)
}
@ -604,3 +605,75 @@ func TestIssue827(t *testing.T) {
term.MustExec("goroutine 1")
})
}
func findCmdName(c *Commands, cmdstr string, prefix cmdPrefix) string {
for _, v := range c.cmds {
if v.match(cmdstr) {
if prefix != noPrefix && v.allowedPrefixes&prefix == 0 {
continue
}
return v.aliases[0]
}
}
return ""
}
func TestConfig(t *testing.T) {
var term Term
term.conf = &config.Config{}
term.cmds = DebugCommands(nil)
err := configureCmd(&term, callContext{}, "nonexistent-parameter 10")
if err == nil {
t.Fatalf("expected error executing configureCmd(nonexistent-parameter)")
}
err = configureCmd(&term, callContext{}, "max-string-len 10")
if err != nil {
t.Fatalf("error executing configureCmd(max-string-len): %v", err)
}
if term.conf.MaxStringLen == nil {
t.Fatalf("expected MaxStringLen 10, got nil")
}
if *term.conf.MaxStringLen != 10 {
t.Fatalf("expected MaxStringLen 10, got: %d", *term.conf.MaxStringLen)
}
err = configureCmd(&term, callContext{}, "substitute-path a b")
if err != nil {
t.Fatalf("error executing configureCmd(substitute-path a b): %v", err)
}
if len(term.conf.SubstitutePath) != 1 || (term.conf.SubstitutePath[0] != config.SubstitutePathRule{"a", "b"}) {
t.Fatalf("unexpected SubstitutePathRules after insert %v", term.conf.SubstitutePath)
}
err = configureCmd(&term, callContext{}, "substitute-path a")
if err != nil {
t.Fatalf("error executing configureCmd(substitute-path a): %v", err)
}
if len(term.conf.SubstitutePath) != 0 {
t.Fatalf("unexpected SubstitutePathRules after delete %v", term.conf.SubstitutePath)
}
err = configureCmd(&term, callContext{}, "alias print blah")
if err != nil {
t.Fatalf("error executing configureCmd(alias print blah): %v", err)
}
if len(term.conf.Aliases["print"]) != 1 {
t.Fatalf("aliases not changed after configure command %v", term.conf.Aliases)
}
if findCmdName(term.cmds, "blah", noPrefix) != "print" {
t.Fatalf("new alias not found")
}
err = configureCmd(&term, callContext{}, "alias blah")
if err != nil {
t.Fatalf("error executing configureCmd(alias blah): %v", err)
}
if len(term.conf.Aliases["print"]) != 0 {
t.Fatalf("alias not removed after configure command %v", term.conf.Aliases)
}
if findCmdName(term.cmds, "blah", noPrefix) != "" {
t.Fatalf("new alias found after delete")
}
}

188
pkg/terminal/config.go Normal file

@ -0,0 +1,188 @@
package terminal
import (
"fmt"
"os"
"reflect"
"strconv"
"strings"
"text/tabwriter"
"github.com/derekparker/delve/pkg/config"
)
func configureCmd(t *Term, ctx callContext, args string) error {
switch args {
case "-list":
return configureList(t)
case "-save":
return config.SaveConfig(t.conf)
case "":
return fmt.Errorf("wrong number of arguments to \"config\"")
default:
return configureSet(t, args)
}
}
type configureIterator struct {
cfgValue reflect.Value
cfgType reflect.Type
i int
}
func iterateConfiguration(conf *config.Config) *configureIterator {
cfgValue := reflect.ValueOf(conf).Elem()
cfgType := cfgValue.Type()
return &configureIterator{cfgValue, cfgType, -1}
}
func (it *configureIterator) Next() bool {
it.i++
return it.i < it.cfgValue.NumField()
}
func (it *configureIterator) Field() (name string, field reflect.Value) {
name = it.cfgType.Field(it.i).Tag.Get("yaml")
if comma := strings.Index(name, ","); comma >= 0 {
name = name[:comma]
}
field = it.cfgValue.Field(it.i)
return
}
func configureFindFieldByName(conf *config.Config, name string) reflect.Value {
it := iterateConfiguration(conf)
for it.Next() {
fieldName, field := it.Field()
if fieldName == name {
return field
}
}
return reflect.ValueOf(nil)
}
func configureList(t *Term) error {
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 1, ' ', 0)
it := iterateConfiguration(t.conf)
for it.Next() {
fieldName, field := it.Field()
if fieldName == "" {
continue
}
if !field.IsNil() {
if field.Kind() == reflect.Ptr {
fmt.Fprintf(w, "%s\t%v\n", fieldName, field.Elem())
} else {
fmt.Fprintf(w, "%s\t%v\n", fieldName, field)
}
} else {
fmt.Fprintf(w, "%s\t<not defined>\n", fieldName)
}
}
return w.Flush()
}
func configureSet(t *Term, args string) error {
v := strings.SplitN(args, " ", 2)
cfgname := v[0]
var rest string
if len(v) == 2 {
rest = v[1]
}
if cfgname == "alias" {
return configureSetAlias(t, rest)
}
field := configureFindFieldByName(t.conf, cfgname)
if !field.CanAddr() {
return fmt.Errorf("%q is not a configuration parameter", cfgname)
}
if field.Kind() == reflect.Slice && field.Type().Elem().Name() == "SubstitutePathRule" {
return configureSetSubstituePath(t, rest)
}
simpleArg := func(typ reflect.Type) (reflect.Value, error) {
switch typ.Kind() {
case reflect.Int:
n, err := strconv.Atoi(rest)
if err != nil {
return reflect.ValueOf(nil), fmt.Errorf("argument to %q must be a number", cfgname)
}
return reflect.ValueOf(&n), nil
default:
return reflect.ValueOf(nil), fmt.Errorf("unsupported type for configuration key %q", cfgname)
}
}
if field.Kind() == reflect.Ptr {
val, err := simpleArg(field.Type().Elem())
if err != nil {
return err
}
field.Set(val)
} else {
val, err := simpleArg(field.Type())
if err != nil {
return err
}
field.Set(val.Elem())
}
return nil
}
func configureSetSubstituePath(t *Term, rest string) error {
argv := config.SplitQuotedFields(rest, '"')
switch len(argv) {
case 1: // delete substitute-path rule
for i := range t.conf.SubstitutePath {
if t.conf.SubstitutePath[i].From == argv[0] {
copy(t.conf.SubstitutePath[i:], t.conf.SubstitutePath[i+1:])
t.conf.SubstitutePath = t.conf.SubstitutePath[:len(t.conf.SubstitutePath)-1]
return nil
}
}
return fmt.Errorf("could not find rule for %q", argv[0])
case 2: // add substitute-path rule
for i := range t.conf.SubstitutePath {
if t.conf.SubstitutePath[i].From == argv[0] {
t.conf.SubstitutePath[i].To = argv[1]
return nil
}
}
t.conf.SubstitutePath = append(t.conf.SubstitutePath, config.SubstitutePathRule{argv[0], argv[1]})
default:
return fmt.Errorf("too many arguments to \"config substitute-path\"")
}
return nil
}
func configureSetAlias(t *Term, rest string) error {
argv := config.SplitQuotedFields(rest, '"')
switch len(argv) {
case 1: // delete alias rule
for k := range t.conf.Aliases {
v := t.conf.Aliases[k]
for i := range v {
if v[i] == argv[0] {
copy(v[i:], v[i+1:])
t.conf.Aliases[k] = v[:len(v)-1]
}
}
}
case 2: // add alias rule
alias, cmd := argv[1], argv[0]
if t.conf.Aliases == nil {
t.conf.Aliases = make(map[string][]string)
}
t.conf.Aliases[cmd] = append(t.conf.Aliases[cmd], alias)
}
t.cmds.Merge(t.conf.Aliases)
return nil
}

@ -14,6 +14,7 @@ import (
"github.com/derekparker/delve/pkg/config"
"github.com/derekparker/delve/service"
"github.com/derekparker/delve/service/api"
)
const (
@ -240,3 +241,18 @@ func (t *Term) handleExit() (int, error) {
}
return 0, nil
}
// loadConfig returns an api.LoadConfig with the parameterss specified in
// the configuration file.
func (t *Term) loadConfig() api.LoadConfig {
r := api.LoadConfig{true, 1, 64, 64, -1}
if t.conf.MaxStringLen != nil {
r.MaxStringLen = *t.conf.MaxStringLen
}
if t.conf.MaxArrayValues != nil {
r.MaxArrayValues = *t.conf.MaxArrayValues
}
return r
}