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