Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 2 additions & 57 deletions pkg/executor/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,62 +172,7 @@ func ReplaceVar(cmd, varName, replacement string, syntax string) string {
return re.ReplaceAllLiteralString(cmd, replacement)
}

// FindAllVars finds ALL variable references in a command, ignoring quoting.
// FindAllVars finds variable references using the parser's syntax and escaping rules.
func FindAllVars(cmd string, syntax string) []string {
allowDollar := syntax == "dollar" || syntax == "both"
allowAngle := syntax == "angle" || syntax == "both"

var vars []string
seen := make(map[string]bool)
add := func(name string) {
if seen[name] {
return
}
seen[name] = true
vars = append(vars, name)
}

for i := 0; i < len(cmd); i++ {
switch cmd[i] {
case '$':
i = scanDollarVar(cmd, i, allowDollar, add)
case '<':
i = scanAngleVar(cmd, i, allowAngle, add)
}
}

return vars
}

func scanDollarVar(cmd string, i int, allowDollar bool, add func(string)) int {
if !allowDollar || i+1 >= len(cmd) || (i > 0 && cmd[i-1] == '\\') {
return i
}
j := i + 1
for j < len(cmd) && parser.IsVarChar(cmd[j], j == i+1) {
j++
}
if j > i+1 {
add(cmd[i+1 : j])
}
return j - 1
}

func scanAngleVar(cmd string, i int, allowAngle bool, add func(string)) int {
if !allowAngle {
return i
}
j := i + 1
if j >= len(cmd) || !parser.IsVarChar(cmd[j], true) {
return i
}
j++
for j < len(cmd) && parser.IsVarChar(cmd[j], false) {
j++
}
if j >= len(cmd) || cmd[j] != '>' {
return i
}
add(cmd[i+1 : j])
return j
return parser.ExtractVars(cmd, syntax == "" || syntax == "dollar" || syntax == "both", syntax == "angle" || syntax == "both")
}
28 changes: 28 additions & 0 deletions pkg/executor/var_discovery_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package executor

import (
"reflect"
"testing"
)

func TestFindAllVarsEscapesAndDefaultSyntax(t *testing.T) {
for _, tc := range []struct {
name, command, syntax string
want []string
}{
{"default", "echo $value", "", []string{"value"}},
{"odd backslash", `echo \$literal $value`, "dollar", []string{"value"}},
{"even backslashes", `echo \\$value`, "dollar", []string{"value"}},
{"three backslashes", `echo \\\$literal $value`, "dollar", []string{"value"}},
{"shell braces", "echo ${HOME} $value", "dollar", []string{"value"}},
{"ordered unique names", "echo $first <second> $first", "both", []string{"first", "second"}},
{"angle only", "echo $first <second>", "angle", []string{"second"}},
{"malformed angle", "echo <broken $value", "both", []string{"value"}},
} {
t.Run(tc.name, func(t *testing.T) {
if got := FindAllVars(tc.command, tc.syntax); !reflect.DeepEqual(got, tc.want) {
t.Fatalf("FindAllVars() = %v, want %v", got, tc.want)
}
})
}
}
56 changes: 40 additions & 16 deletions pkg/parser/compose.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
package parser

import (
"regexp"
)

var combinedRegex = regexp.MustCompile(`\$([a-zA-Z_][a-zA-Z0-9_]*)|<([a-zA-Z_][a-zA-Z0-9_]*)>`)
// WalkVars visits complete variable references in source order. Offsets are byte
// offsets into command. Dollar references escaped by an odd number of preceding
// backslashes and shell ${...} forms are left to the shell.
func WalkVars(command string, allowDollar, allowAngle bool, visit func(start, end int, name string)) {
backslashes := 0
for i := 0; i < len(command); i++ {
if command[i] == '\\' {
backslashes++
continue
}
escaped := backslashes%2 != 0
backslashes = 0
dollar := command[i] == '$' && allowDollar && !escaped
angle := command[i] == '<' && allowAngle
if !dollar && !angle {
continue
}
start := i
j := i + 1
if j >= len(command) || !IsVarChar(command[j], true) {
continue
}
j++
for j < len(command) && IsVarChar(command[j], false) {
j++
}
name := command[i+1 : j]
if angle {
if j >= len(command) || command[j] != '>' {
continue
}
j++
}
visit(start, j, name)
i = j - 1
}
}

// ExtractVars finds all variables in a command string. It respects the provided
// flags for dollar ($var) and angle bracket (<var>) syntaxes. It returns a
Expand All @@ -14,20 +46,12 @@ func ExtractVars(command string, allowDollar, allowAngle bool) []string {
varMap := make(map[string]bool)
var vars []string

matches := combinedRegex.FindAllStringSubmatch(command, -1)
for _, match := range matches {
var name string
if match[1] != "" && allowDollar {
name = match[1]
} else if match[2] != "" && allowAngle {
name = match[2]
}

if name != "" && !varMap[name] {
WalkVars(command, allowDollar, allowAngle, func(_, _ int, name string) {
if !varMap[name] {
varMap[name] = true
vars = append(vars, name)
}
}
})

return vars
}
4 changes: 2 additions & 2 deletions pkg/parser/compose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ func TestExtractVars(t *testing.T) {
want: nil,
},
{
name: "escaped variables", // the parser currently extracts it anyway, which is fine for compose
name: "escaped variables",
command: "echo \\$var",
want: []string{"var"},
want: nil,
},
}

Expand Down