shell_parser.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. package dockerfile
  2. // This will take a single word and an array of env variables and
  3. // process all quotes (" and ') as well as $xxx and ${xxx} env variable
  4. // tokens. Tries to mimic bash shell process.
  5. // It doesn't support all flavors of ${xx:...} formats but new ones can
  6. // be added by adding code to the "special ${} format processing" section
  7. import (
  8. "fmt"
  9. "strings"
  10. "unicode"
  11. )
  12. type shellWord struct {
  13. word string
  14. envs []string
  15. pos int
  16. }
  17. // ProcessWord will use the 'env' list of environment variables,
  18. // and replace any env var references in 'word'.
  19. func ProcessWord(word string, env []string) (string, error) {
  20. sw := &shellWord{
  21. word: word,
  22. envs: env,
  23. pos: 0,
  24. }
  25. return sw.process()
  26. }
  27. func (sw *shellWord) process() (string, error) {
  28. return sw.processStopOn('\000')
  29. }
  30. // Process the word, starting at 'pos', and stop when we get to the
  31. // end of the word or the 'stopChar' character
  32. func (sw *shellWord) processStopOn(stopChar rune) (string, error) {
  33. var result string
  34. var charFuncMapping = map[rune]func() (string, error){
  35. '\'': sw.processSingleQuote,
  36. '"': sw.processDoubleQuote,
  37. '$': sw.processDollar,
  38. }
  39. for sw.pos < len(sw.word) {
  40. ch := sw.peek()
  41. if stopChar != '\000' && ch == stopChar {
  42. sw.next()
  43. break
  44. }
  45. if fn, ok := charFuncMapping[ch]; ok {
  46. // Call special processing func for certain chars
  47. tmp, err := fn()
  48. if err != nil {
  49. return "", err
  50. }
  51. result += tmp
  52. } else {
  53. // Not special, just add it to the result
  54. ch = sw.next()
  55. if ch == '\\' {
  56. // '\' escapes, except end of line
  57. ch = sw.next()
  58. if ch == '\000' {
  59. continue
  60. }
  61. }
  62. result += string(ch)
  63. }
  64. }
  65. return result, nil
  66. }
  67. func (sw *shellWord) peek() rune {
  68. if sw.pos == len(sw.word) {
  69. return '\000'
  70. }
  71. return rune(sw.word[sw.pos])
  72. }
  73. func (sw *shellWord) next() rune {
  74. if sw.pos == len(sw.word) {
  75. return '\000'
  76. }
  77. ch := rune(sw.word[sw.pos])
  78. sw.pos++
  79. return ch
  80. }
  81. func (sw *shellWord) processSingleQuote() (string, error) {
  82. // All chars between single quotes are taken as-is
  83. // Note, you can't escape '
  84. var result string
  85. sw.next()
  86. for {
  87. ch := sw.next()
  88. if ch == '\000' || ch == '\'' {
  89. break
  90. }
  91. result += string(ch)
  92. }
  93. return result, nil
  94. }
  95. func (sw *shellWord) processDoubleQuote() (string, error) {
  96. // All chars up to the next " are taken as-is, even ', except any $ chars
  97. // But you can escape " with a \
  98. var result string
  99. sw.next()
  100. for sw.pos < len(sw.word) {
  101. ch := sw.peek()
  102. if ch == '"' {
  103. sw.next()
  104. break
  105. }
  106. if ch == '$' {
  107. tmp, err := sw.processDollar()
  108. if err != nil {
  109. return "", err
  110. }
  111. result += tmp
  112. } else {
  113. ch = sw.next()
  114. if ch == '\\' {
  115. chNext := sw.peek()
  116. if chNext == '\000' {
  117. // Ignore \ at end of word
  118. continue
  119. }
  120. if chNext == '"' || chNext == '$' {
  121. // \" and \$ can be escaped, all other \'s are left as-is
  122. ch = sw.next()
  123. }
  124. }
  125. result += string(ch)
  126. }
  127. }
  128. return result, nil
  129. }
  130. func (sw *shellWord) processDollar() (string, error) {
  131. sw.next()
  132. ch := sw.peek()
  133. if ch == '{' {
  134. sw.next()
  135. name := sw.processName()
  136. ch = sw.peek()
  137. if ch == '}' {
  138. // Normal ${xx} case
  139. sw.next()
  140. return sw.getEnv(name), nil
  141. }
  142. if ch == ':' {
  143. // Special ${xx:...} format processing
  144. // Yes it allows for recursive $'s in the ... spot
  145. sw.next() // skip over :
  146. modifier := sw.next()
  147. word, err := sw.processStopOn('}')
  148. if err != nil {
  149. return "", err
  150. }
  151. // Grab the current value of the variable in question so we
  152. // can use to to determine what to do based on the modifier
  153. newValue := sw.getEnv(name)
  154. switch modifier {
  155. case '+':
  156. if newValue != "" {
  157. newValue = word
  158. }
  159. return newValue, nil
  160. case '-':
  161. if newValue == "" {
  162. newValue = word
  163. }
  164. return newValue, nil
  165. default:
  166. return "", fmt.Errorf("Unsupported modifier (%c) in substitution: %s", modifier, sw.word)
  167. }
  168. }
  169. return "", fmt.Errorf("Missing ':' in substitution: %s", sw.word)
  170. }
  171. // $xxx case
  172. name := sw.processName()
  173. if name == "" {
  174. return "$", nil
  175. }
  176. return sw.getEnv(name), nil
  177. }
  178. func (sw *shellWord) processName() string {
  179. // Read in a name (alphanumeric or _)
  180. // If it starts with a numeric then just return $#
  181. var name string
  182. for sw.pos < len(sw.word) {
  183. ch := sw.peek()
  184. if len(name) == 0 && unicode.IsDigit(ch) {
  185. ch = sw.next()
  186. return string(ch)
  187. }
  188. if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
  189. break
  190. }
  191. ch = sw.next()
  192. name += string(ch)
  193. }
  194. return name
  195. }
  196. func (sw *shellWord) getEnv(name string) string {
  197. for _, env := range sw.envs {
  198. i := strings.Index(env, "=")
  199. if i < 0 {
  200. if name == env {
  201. // Should probably never get here, but just in case treat
  202. // it like "var" and "var=" are the same
  203. return ""
  204. }
  205. continue
  206. }
  207. if name != env[:i] {
  208. continue
  209. }
  210. return env[i+1:]
  211. }
  212. return ""
  213. }