]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: quote expanded shell variables used within regular expressions
authorBryan C. Mills <bcmills@google.com>
Fri, 1 Mar 2019 15:09:02 +0000 (10:09 -0500)
committerBryan C. Mills <bcmills@google.com>
Fri, 1 Mar 2019 20:53:50 +0000 (20:53 +0000)
We mostly use shell variables for paths, and we don't want file paths
like "C:\work\go1.4" to turn into regular expressions.

Updates #30228
Updates #30241

Change-Id: If18b775b2f8b2821eaf197c4be4a322066af839f
Reviewed-on: https://go-review.googlesource.com/c/164626
Run-TryBot: Bryan C. Mills <bcmills@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
src/cmd/go/script_test.go

index e204471beb8d61fff05e387c4a49ce1aec47b410..48420daa1f882eb691c5726cd29bf63bbc0cd303 100644 (file)
@@ -192,7 +192,7 @@ func (ts *testScript) run() {
        a, err := txtar.ParseFile(ts.file)
        ts.check(err)
        for _, f := range a.Files {
-               name := ts.mkabs(ts.expand(f.Name))
+               name := ts.mkabs(ts.expand(f.Name, false))
                ts.check(os.MkdirAll(filepath.Dir(name), 0777))
                ts.check(ioutil.WriteFile(name, f.Data, 0666))
        }
@@ -238,34 +238,24 @@ Script:
                }
 
                // Parse input line. Ignore blanks entirely.
-               args := ts.parse(line)
-               if len(args) == 0 {
+               parsed := ts.parse(line)
+               if parsed.name == "" {
+                       if parsed.neg || len(parsed.conds) > 0 {
+                               ts.fatalf("missing command")
+                       }
                        continue
                }
 
                // Echo command to log.
                fmt.Fprintf(&ts.log, "> %s\n", line)
 
-               // Command prefix [cond] means only run this command if cond is satisfied.
-               for strings.HasPrefix(args[0], "[") && strings.HasSuffix(args[0], "]") {
-                       cond := args[0]
-                       cond = cond[1 : len(cond)-1]
-                       cond = strings.TrimSpace(cond)
-                       args = args[1:]
-                       if len(args) == 0 {
-                               ts.fatalf("missing command after condition")
-                       }
-                       want := true
-                       if strings.HasPrefix(cond, "!") {
-                               want = false
-                               cond = strings.TrimSpace(cond[1:])
-                       }
+               for _, cond := range parsed.conds {
                        // Known conds are: $GOOS, $GOARCH, runtime.Compiler, and 'short' (for testing.Short).
                        //
                        // NOTE: If you make changes here, update testdata/script/README too!
                        //
                        ok := false
-                       switch cond {
+                       switch cond.tag {
                        case runtime.GOOS, runtime.GOARCH, runtime.Compiler:
                                ok = true
                        case "short":
@@ -285,8 +275,8 @@ Script:
                        case "symlink":
                                ok = testenv.HasSymlink()
                        default:
-                               if strings.HasPrefix(cond, "exec:") {
-                                       prog := cond[len("exec:"):]
+                               if strings.HasPrefix(cond.tag, "exec:") {
+                                       prog := cond.tag[len("exec:"):]
                                        ok = execCache.Do(prog, func() interface{} {
                                                if runtime.GOOS == "plan9" && prog == "git" {
                                                        // The Git command is usually not the real Git on Plan 9.
@@ -298,33 +288,22 @@ Script:
                                        }).(bool)
                                        break
                                }
-                               if !imports.KnownArch[cond] && !imports.KnownOS[cond] && cond != "gc" && cond != "gccgo" {
-                                       ts.fatalf("unknown condition %q", cond)
+                               if !imports.KnownArch[cond.tag] && !imports.KnownOS[cond.tag] && cond.tag != "gc" && cond.tag != "gccgo" {
+                                       ts.fatalf("unknown condition %q", cond.tag)
                                }
                        }
-                       if ok != want {
+                       if ok != cond.want {
                                // Don't run rest of line.
                                continue Script
                        }
                }
 
-               // Command prefix ! means negate the expectations about this command:
-               // go command should fail, match should not be found, etc.
-               neg := false
-               if args[0] == "!" {
-                       neg = true
-                       args = args[1:]
-                       if len(args) == 0 {
-                               ts.fatalf("! on line by itself")
-                       }
-               }
-
                // Run command.
-               cmd := scriptCmds[args[0]]
+               cmd := scriptCmds[parsed.name]
                if cmd == nil {
-                       ts.fatalf("unknown command %q", args[0])
+                       ts.fatalf("unknown command %q", parsed.name)
                }
-               cmd(ts, neg, args[1:])
+               cmd(ts, parsed.neg, parsed.args)
 
                // Command can ask script to stop early.
                if ts.stopped {
@@ -376,6 +355,14 @@ var scriptCmds = map[string]func(*testScript, bool, []string){
        "wait":    (*testScript).cmdWait,
 }
 
+// When expanding shell variables for these commands, we apply regexp quoting to
+// expanded strings within the first argument.
+var regexpCmd = map[string]bool{
+       "grep":   true,
+       "stderr": true,
+       "stdout": true,
+}
+
 // addcrlf adds CRLF line endings to the named files.
 func (ts *testScript) cmdAddcrlf(neg bool, args []string) {
        if len(args) == 0 {
@@ -486,8 +473,8 @@ func (ts *testScript) doCmdCmp(args []string, env bool) {
        text2 = string(data)
 
        if env {
-               text1 = ts.expand(text1)
-               text2 = ts.expand(text2)
+               text1 = ts.expand(text1, false)
+               text2 = ts.expand(text2, false)
        }
 
        if text1 == text2 {
@@ -765,9 +752,11 @@ func scriptMatch(ts *testScript, neg bool, args []string, text, name string) {
                ts.fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage)
        }
 
-       pattern := args[0]
-       re, err := regexp.Compile(`(?m)` + pattern)
-       ts.check(err)
+       pattern := `(?m)` + args[0]
+       re, err := regexp.Compile(pattern)
+       if err != nil {
+               ts.fatalf("regexp.Compile(%q): %v", pattern, err)
+       }
 
        isGrep := name == "grep"
        if isGrep {
@@ -956,8 +945,20 @@ func interruptProcess(p *os.Process) {
 }
 
 // expand applies environment variable expansion to the string s.
-func (ts *testScript) expand(s string) string {
-       return os.Expand(s, func(key string) string { return ts.envMap[key] })
+func (ts *testScript) expand(s string, inRegexp bool) string {
+       return os.Expand(s, func(key string) string {
+               e := ts.envMap[key]
+               if inRegexp {
+                       // Replace workdir with $WORK, since we have done the same substitution in
+                       // the text we're about to compare against.
+                       e = strings.ReplaceAll(e, ts.workdir, "$WORK")
+
+                       // Quote to literal strings: we want paths like C:\work\go1.4 to remain
+                       // paths rather than regular expressions.
+                       e = regexp.QuoteMeta(e)
+               }
+               return e
+       })
 }
 
 // fatalf aborts the test with the given failure message.
@@ -975,27 +976,82 @@ func (ts *testScript) mkabs(file string) string {
        return filepath.Join(ts.cd, file)
 }
 
+// A condition guards execution of a command.
+type condition struct {
+       want bool
+       tag  string
+}
+
+// A command is a complete command parsed from a script.
+type command struct {
+       neg   bool        // if true, expect the command to fail
+       conds []condition // all must be satisfied
+       name  string      // the name of the command; must be non-empty
+       args  []string    // shell-expanded arguments following name
+}
+
 // parse parses a single line as a list of space-separated arguments
 // subject to environment variable expansion (but not resplitting).
 // Single quotes around text disable splitting and expansion.
 // To embed a single quote, double it: 'Don''t communicate by sharing memory.'
-func (ts *testScript) parse(line string) []string {
+func (ts *testScript) parse(line string) command {
        ts.line = line
 
        var (
-               args   []string
-               arg    string  // text of current arg so far (need to add line[start:i])
-               start  = -1    // if >= 0, position where current arg text chunk starts
-               quoted = false // currently processing quoted text
+               cmd      command
+               arg      string  // text of current arg so far (need to add line[start:i])
+               start    = -1    // if >= 0, position where current arg text chunk starts
+               quoted   = false // currently processing quoted text
+               isRegexp = false // currently processing unquoted regular expression
        )
+
+       flushArg := func() {
+               defer func() {
+                       arg = ""
+                       start = -1
+               }()
+
+               if cmd.name != "" {
+                       cmd.args = append(cmd.args, arg)
+                       isRegexp = false // Commands take only one regexp argument, so no subsequent args are regexps.
+                       return
+               }
+
+               // Command prefix ! means negate the expectations about this command:
+               // go command should fail, match should not be found, etc.
+               if arg == "!" {
+                       if cmd.neg {
+                               ts.fatalf("duplicated '!' token")
+                       }
+                       cmd.neg = true
+                       return
+               }
+
+               // Command prefix [cond] means only run this command if cond is satisfied.
+               if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
+                       want := true
+                       arg = strings.TrimSpace(arg[1 : len(arg)-1])
+                       if strings.HasPrefix(arg, "!") {
+                               want = false
+                               arg = strings.TrimSpace(arg[1:])
+                       }
+                       if arg == "" {
+                               ts.fatalf("empty condition")
+                       }
+                       cmd.conds = append(cmd.conds, condition{want: want, tag: arg})
+                       return
+               }
+
+               cmd.name = arg
+               isRegexp = regexpCmd[cmd.name]
+       }
+
        for i := 0; ; i++ {
                if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') {
                        // Found arg-separating space.
                        if start >= 0 {
-                               arg += ts.expand(line[start:i])
-                               args = append(args, arg)
-                               start = -1
-                               arg = ""
+                               arg += ts.expand(line[start:i], isRegexp)
+                               flushArg()
                        }
                        if i >= len(line) || line[i] == '#' {
                                break
@@ -1009,7 +1065,7 @@ func (ts *testScript) parse(line string) []string {
                        if !quoted {
                                // starting a quoted chunk
                                if start >= 0 {
-                                       arg += ts.expand(line[start:i])
+                                       arg += ts.expand(line[start:i], isRegexp)
                                }
                                start = i + 1
                                quoted = true
@@ -1033,7 +1089,7 @@ func (ts *testScript) parse(line string) []string {
                        start = i
                }
        }
-       return args
+       return cmd
 }
 
 // diff returns a formatted diff of the two texts,