Commit b45f5b5e authored by Bryan C. Mills's avatar Bryan C. Mills

cmd/go: quote expanded shell variables used within regular expressions

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: default avatarIan Lance Taylor <iant@golang.org>
Reviewed-by: default avatarJay Conrod <jayconrod@google.com>
parent 45861a64
......@@ -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,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment