shell_parser.go 6.6 KB

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