shell_parser.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  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. "text/scanner"
  11. "unicode"
  12. )
  13. type shellWord struct {
  14. word string
  15. scanner scanner.Scanner
  16. envs []string
  17. pos int
  18. escapeToken rune
  19. }
  20. // ProcessWord will use the 'env' list of environment variables,
  21. // and replace any env var references in 'word'.
  22. func ProcessWord(word string, env []string, escapeToken rune) (string, error) {
  23. word, _, err := process(word, env, escapeToken)
  24. return word, err
  25. }
  26. // ProcessWords will use the 'env' list of environment variables,
  27. // and replace any env var references in 'word' then it will also
  28. // return a slice of strings which represents the 'word'
  29. // split up based on spaces - taking into account quotes. Note that
  30. // this splitting is done **after** the env var substitutions are done.
  31. // Note, each one is trimmed to remove leading and trailing spaces (unless
  32. // they are quoted", but ProcessWord retains spaces between words.
  33. func ProcessWords(word string, env []string, escapeToken rune) ([]string, error) {
  34. _, words, err := process(word, env, escapeToken)
  35. return words, err
  36. }
  37. func process(word string, env []string, escapeToken rune) (string, []string, error) {
  38. sw := &shellWord{
  39. word: word,
  40. envs: env,
  41. pos: 0,
  42. escapeToken: escapeToken,
  43. }
  44. sw.scanner.Init(strings.NewReader(word))
  45. return sw.process()
  46. }
  47. func (sw *shellWord) process() (string, []string, error) {
  48. return sw.processStopOn(scanner.EOF)
  49. }
  50. type wordsStruct struct {
  51. word string
  52. words []string
  53. inWord bool
  54. }
  55. func (w *wordsStruct) addChar(ch rune) {
  56. if unicode.IsSpace(ch) && w.inWord {
  57. if len(w.word) != 0 {
  58. w.words = append(w.words, w.word)
  59. w.word = ""
  60. w.inWord = false
  61. }
  62. } else if !unicode.IsSpace(ch) {
  63. w.addRawChar(ch)
  64. }
  65. }
  66. func (w *wordsStruct) addRawChar(ch rune) {
  67. w.word += string(ch)
  68. w.inWord = true
  69. }
  70. func (w *wordsStruct) addString(str string) {
  71. var scan scanner.Scanner
  72. scan.Init(strings.NewReader(str))
  73. for scan.Peek() != scanner.EOF {
  74. w.addChar(scan.Next())
  75. }
  76. }
  77. func (w *wordsStruct) addRawString(str string) {
  78. w.word += str
  79. w.inWord = true
  80. }
  81. func (w *wordsStruct) getWords() []string {
  82. if len(w.word) > 0 {
  83. w.words = append(w.words, w.word)
  84. // Just in case we're called again by mistake
  85. w.word = ""
  86. w.inWord = false
  87. }
  88. return w.words
  89. }
  90. // Process the word, starting at 'pos', and stop when we get to the
  91. // end of the word or the 'stopChar' character
  92. func (sw *shellWord) processStopOn(stopChar rune) (string, []string, error) {
  93. var result string
  94. var words wordsStruct
  95. var charFuncMapping = map[rune]func() (string, error){
  96. '\'': sw.processSingleQuote,
  97. '"': sw.processDoubleQuote,
  98. '$': sw.processDollar,
  99. }
  100. for sw.scanner.Peek() != scanner.EOF {
  101. ch := sw.scanner.Peek()
  102. if stopChar != scanner.EOF && ch == stopChar {
  103. sw.scanner.Next()
  104. break
  105. }
  106. if fn, ok := charFuncMapping[ch]; ok {
  107. // Call special processing func for certain chars
  108. tmp, err := fn()
  109. if err != nil {
  110. return "", []string{}, err
  111. }
  112. result += tmp
  113. if ch == rune('$') {
  114. words.addString(tmp)
  115. } else {
  116. words.addRawString(tmp)
  117. }
  118. } else {
  119. // Not special, just add it to the result
  120. ch = sw.scanner.Next()
  121. if ch == sw.escapeToken {
  122. // '\' (default escape token, but ` allowed) escapes, except end of line
  123. ch = sw.scanner.Next()
  124. if ch == scanner.EOF {
  125. break
  126. }
  127. words.addRawChar(ch)
  128. } else {
  129. words.addChar(ch)
  130. }
  131. result += string(ch)
  132. }
  133. }
  134. return result, words.getWords(), nil
  135. }
  136. func (sw *shellWord) processSingleQuote() (string, error) {
  137. // All chars between single quotes are taken as-is
  138. // Note, you can't escape '
  139. var result string
  140. sw.scanner.Next()
  141. for {
  142. ch := sw.scanner.Next()
  143. if ch == '\'' || ch == scanner.EOF {
  144. break
  145. }
  146. result += string(ch)
  147. }
  148. return result, nil
  149. }
  150. func (sw *shellWord) processDoubleQuote() (string, error) {
  151. // All chars up to the next " are taken as-is, even ', except any $ chars
  152. // But you can escape " with a \ (or ` if escape token set accordingly)
  153. var result string
  154. sw.scanner.Next()
  155. for sw.scanner.Peek() != scanner.EOF {
  156. ch := sw.scanner.Peek()
  157. if ch == '"' {
  158. sw.scanner.Next()
  159. break
  160. }
  161. if ch == '$' {
  162. tmp, err := sw.processDollar()
  163. if err != nil {
  164. return "", err
  165. }
  166. result += tmp
  167. } else {
  168. ch = sw.scanner.Next()
  169. if ch == sw.escapeToken {
  170. chNext := sw.scanner.Peek()
  171. if chNext == scanner.EOF {
  172. // Ignore \ at end of word
  173. continue
  174. }
  175. if chNext == '"' || chNext == '$' {
  176. // \" and \$ can be escaped, all other \'s are left as-is
  177. ch = sw.scanner.Next()
  178. }
  179. }
  180. result += string(ch)
  181. }
  182. }
  183. return result, nil
  184. }
  185. func (sw *shellWord) processDollar() (string, error) {
  186. sw.scanner.Next()
  187. ch := sw.scanner.Peek()
  188. if ch == '{' {
  189. sw.scanner.Next()
  190. name := sw.processName()
  191. ch = sw.scanner.Peek()
  192. if ch == '}' {
  193. // Normal ${xx} case
  194. sw.scanner.Next()
  195. return sw.getEnv(name), nil
  196. }
  197. if ch == ':' {
  198. // Special ${xx:...} format processing
  199. // Yes it allows for recursive $'s in the ... spot
  200. sw.scanner.Next() // skip over :
  201. modifier := sw.scanner.Next()
  202. word, _, err := sw.processStopOn('}')
  203. if err != nil {
  204. return "", err
  205. }
  206. // Grab the current value of the variable in question so we
  207. // can use to to determine what to do based on the modifier
  208. newValue := sw.getEnv(name)
  209. switch modifier {
  210. case '+':
  211. if newValue != "" {
  212. newValue = word
  213. }
  214. return newValue, nil
  215. case '-':
  216. if newValue == "" {
  217. newValue = word
  218. }
  219. return newValue, nil
  220. default:
  221. return "", fmt.Errorf("Unsupported modifier (%c) in substitution: %s", modifier, sw.word)
  222. }
  223. }
  224. return "", fmt.Errorf("Missing ':' in substitution: %s", sw.word)
  225. }
  226. // $xxx case
  227. name := sw.processName()
  228. if name == "" {
  229. return "$", nil
  230. }
  231. return sw.getEnv(name), nil
  232. }
  233. func (sw *shellWord) processName() string {
  234. // Read in a name (alphanumeric or _)
  235. // If it starts with a numeric then just return $#
  236. var name string
  237. for sw.scanner.Peek() != scanner.EOF {
  238. ch := sw.scanner.Peek()
  239. if len(name) == 0 && unicode.IsDigit(ch) {
  240. ch = sw.scanner.Next()
  241. return string(ch)
  242. }
  243. if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
  244. break
  245. }
  246. ch = sw.scanner.Next()
  247. name += string(ch)
  248. }
  249. return name
  250. }
  251. func (sw *shellWord) getEnv(name string) string {
  252. for _, env := range sw.envs {
  253. i := strings.Index(env, "=")
  254. if i < 0 {
  255. if equalEnvKeys(name, env) {
  256. // Should probably never get here, but just in case treat
  257. // it like "var" and "var=" are the same
  258. return ""
  259. }
  260. continue
  261. }
  262. compareName := env[:i]
  263. if !equalEnvKeys(name, compareName) {
  264. continue
  265. }
  266. return env[i+1:]
  267. }
  268. return ""
  269. }