mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Fix some escaping around env var processing
Clarify in the docs that ENV is not recursive Closes #10391 Signed-off-by: Doug Davis <dug@us.ibm.com>
This commit is contained in:
parent
e3e6f8e859
commit
6d66e3e7a5
10 changed files with 402 additions and 66 deletions
209
builder/shell_parser.go
Normal file
209
builder/shell_parser.go
Normal file
|
@ -0,0 +1,209 @@
|
|||
package builder
|
||||
|
||||
// This will take a single word and an array of env variables and
|
||||
// process all quotes (" and ') as well as $xxx and ${xxx} env variable
|
||||
// tokens. Tries to mimic bash shell process.
|
||||
// It doesn't support all flavors of ${xx:...} formats but new ones can
|
||||
// be added by adding code to the "special ${} format processing" section
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type shellWord struct {
|
||||
word string
|
||||
envs []string
|
||||
pos int
|
||||
}
|
||||
|
||||
func ProcessWord(word string, env []string) (string, error) {
|
||||
sw := &shellWord{
|
||||
word: word,
|
||||
envs: env,
|
||||
pos: 0,
|
||||
}
|
||||
return sw.process()
|
||||
}
|
||||
|
||||
func (sw *shellWord) process() (string, error) {
|
||||
return sw.processStopOn('\000')
|
||||
}
|
||||
|
||||
// Process the word, starting at 'pos', and stop when we get to the
|
||||
// end of the word or the 'stopChar' character
|
||||
func (sw *shellWord) processStopOn(stopChar rune) (string, error) {
|
||||
var result string
|
||||
var charFuncMapping = map[rune]func() (string, error){
|
||||
'\'': sw.processSingleQuote,
|
||||
'"': sw.processDoubleQuote,
|
||||
'$': sw.processDollar,
|
||||
}
|
||||
|
||||
for sw.pos < len(sw.word) {
|
||||
ch := sw.peek()
|
||||
if stopChar != '\000' && ch == stopChar {
|
||||
sw.next()
|
||||
break
|
||||
}
|
||||
if fn, ok := charFuncMapping[ch]; ok {
|
||||
// Call special processing func for certain chars
|
||||
tmp, err := fn()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result += tmp
|
||||
} else {
|
||||
// Not special, just add it to the result
|
||||
ch = sw.next()
|
||||
if ch == '\\' {
|
||||
// '\' escapes, except end of line
|
||||
ch = sw.next()
|
||||
if ch == '\000' {
|
||||
continue
|
||||
}
|
||||
}
|
||||
result += string(ch)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (sw *shellWord) peek() rune {
|
||||
if sw.pos == len(sw.word) {
|
||||
return '\000'
|
||||
}
|
||||
return rune(sw.word[sw.pos])
|
||||
}
|
||||
|
||||
func (sw *shellWord) next() rune {
|
||||
if sw.pos == len(sw.word) {
|
||||
return '\000'
|
||||
}
|
||||
ch := rune(sw.word[sw.pos])
|
||||
sw.pos++
|
||||
return ch
|
||||
}
|
||||
|
||||
func (sw *shellWord) processSingleQuote() (string, error) {
|
||||
// All chars between single quotes are taken as-is
|
||||
// Note, you can't escape '
|
||||
var result string
|
||||
|
||||
sw.next()
|
||||
|
||||
for {
|
||||
ch := sw.next()
|
||||
if ch == '\000' || ch == '\'' {
|
||||
break
|
||||
}
|
||||
result += string(ch)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (sw *shellWord) processDoubleQuote() (string, error) {
|
||||
// All chars up to the next " are taken as-is, even ', except any $ chars
|
||||
// But you can escape " with a \
|
||||
var result string
|
||||
|
||||
sw.next()
|
||||
|
||||
for sw.pos < len(sw.word) {
|
||||
ch := sw.peek()
|
||||
if ch == '"' {
|
||||
sw.next()
|
||||
break
|
||||
}
|
||||
if ch == '$' {
|
||||
tmp, err := sw.processDollar()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result += tmp
|
||||
} else {
|
||||
ch = sw.next()
|
||||
if ch == '\\' {
|
||||
chNext := sw.peek()
|
||||
|
||||
if chNext == '\000' {
|
||||
// Ignore \ at end of word
|
||||
continue
|
||||
}
|
||||
|
||||
if chNext == '"' || chNext == '$' {
|
||||
// \" and \$ can be escaped, all other \'s are left as-is
|
||||
ch = sw.next()
|
||||
}
|
||||
}
|
||||
result += string(ch)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (sw *shellWord) processDollar() (string, error) {
|
||||
sw.next()
|
||||
ch := sw.peek()
|
||||
if ch == '{' {
|
||||
sw.next()
|
||||
name := sw.processName()
|
||||
ch = sw.peek()
|
||||
if ch == '}' {
|
||||
// Normal ${xx} case
|
||||
sw.next()
|
||||
return sw.getEnv(name), nil
|
||||
}
|
||||
return "", fmt.Errorf("Unsupported ${} substitution: %s", sw.word)
|
||||
} else {
|
||||
// $xxx case
|
||||
name := sw.processName()
|
||||
if name == "" {
|
||||
return "$", nil
|
||||
}
|
||||
return sw.getEnv(name), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (sw *shellWord) processName() string {
|
||||
// Read in a name (alphanumeric or _)
|
||||
// If it starts with a numeric then just return $#
|
||||
var name string
|
||||
|
||||
for sw.pos < len(sw.word) {
|
||||
ch := sw.peek()
|
||||
if len(name) == 0 && unicode.IsDigit(ch) {
|
||||
ch = sw.next()
|
||||
return string(ch)
|
||||
}
|
||||
if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
|
||||
break
|
||||
}
|
||||
ch = sw.next()
|
||||
name += string(ch)
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func (sw *shellWord) getEnv(name string) string {
|
||||
for _, env := range sw.envs {
|
||||
i := strings.Index(env, "=")
|
||||
if i < 0 {
|
||||
if name == env {
|
||||
// Should probably never get here, but just in case treat
|
||||
// it like "var" and "var=" are the same
|
||||
return ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
if name != env[:i] {
|
||||
continue
|
||||
}
|
||||
return env[i+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue