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 "" }