diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index 9ebcebf8..8ed412ee 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -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 + +Changes the value of a configuration parameter. + + config subistitute-path + config subistitute-path + +Adds or removes a path subistitution rule. + + config alias + config alias + +Defines as an alias to or removes an alias. + + ## continue Run until breakpoint or program termination. diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index 59c1cdaf..1f928b43 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -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) { diff --git a/cmd/dlv/cmds/commands_test.go b/cmd/dlv/cmds/commands_test.go deleted file mode 100644 index 6205b8b7..00000000 --- a/cmd/dlv/cmds/commands_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/pkg/config/config.go b/pkg/config/config.go index b2ef3164..35d2a63a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 { diff --git a/pkg/config/split.go b/pkg/config/split.go new file mode 100644 index 00000000..6ab0e063 --- /dev/null +++ b/pkg/config/split.go @@ -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 +} diff --git a/pkg/config/split_test.go b/pkg/config/split_test.go new file mode 100644 index 00000000..c49283b0 --- /dev/null +++ b/pkg/config/split_test.go @@ -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) + } + } +} diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 04ca46eb..a09d2ab1 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -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 . 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 + +Changes the value of a configuration parameter. + + config subistitute-path + config subistitute-path + +Adds or removes a path subistitution rule. + + config alias + config alias + +Defines as an alias to 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 diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go index cbf65a4d..8cc77c1d 100644 --- a/pkg/terminal/command_test.go +++ b/pkg/terminal/command_test.go @@ -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") + } +} diff --git a/pkg/terminal/config.go b/pkg/terminal/config.go new file mode 100644 index 00000000..bb034c35 --- /dev/null +++ b/pkg/terminal/config.go @@ -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\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 +} diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go index b724f445..9bd7f55d 100644 --- a/pkg/terminal/terminal.go +++ b/pkg/terminal/terminal.go @@ -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 +}