shell_parser.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. package builder
  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. func ProcessWord(word string, env []string) (string, error) {
  18. sw := &shellWord{
  19. word: word,
  20. envs: env,
  21. pos: 0,
  22. }
  23. return sw.process()
  24. }
  25. func (sw *shellWord) process() (string, error) {
  26. return sw.processStopOn('\000')
  27. }
  28. // Process the word, starting at 'pos', and stop when we get to the
  29. // end of the word or the 'stopChar' character
  30. func (sw *shellWord) processStopOn(stopChar rune) (string, error) {
  31. var result string
  32. var charFuncMapping = map[rune]func() (string, error){
  33. '\'': sw.processSingleQuote,
  34. '"': sw.processDoubleQuote,
  35. '$': sw.processDollar,
  36. }
  37. for sw.pos < len(sw.word) {
  38. ch := sw.peek()
  39. if stopChar != '\000' && ch == stopChar {
  40. sw.next()
  41. break
  42. }
  43. if fn, ok := charFuncMapping[ch]; ok {
  44. // Call special processing func for certain chars
  45. tmp, err := fn()
  46. if err != nil {
  47. return "", err
  48. }
  49. result += tmp
  50. } else {
  51. // Not special, just add it to the result
  52. ch = sw.next()
  53. if ch == '\\' {
  54. // '\' escapes, except end of line
  55. ch = sw.next()
  56. if ch == '\000' {
  57. continue
  58. }
  59. }
  60. result += string(ch)
  61. }
  62. }
  63. return result, nil
  64. }
  65. func (sw *shellWord) peek() rune {
  66. if sw.pos == len(sw.word) {
  67. return '\000'
  68. }
  69. return rune(sw.word[sw.pos])
  70. }
  71. func (sw *shellWord) next() rune {
  72. if sw.pos == len(sw.word) {
  73. return '\000'
  74. }
  75. ch := rune(sw.word[sw.pos])
  76. sw.pos++
  77. return ch
  78. }
  79. func (sw *shellWord) processSingleQuote() (string, error) {
  80. // All chars between single quotes are taken as-is
  81. // Note, you can't escape '
  82. var result string
  83. sw.next()
  84. for {
  85. ch := sw.next()
  86. if ch == '\000' || ch == '\'' {
  87. break
  88. }
  89. result += string(ch)
  90. }
  91. return result, nil
  92. }
  93. func (sw *shellWord) processDoubleQuote() (string, error) {
  94. // All chars up to the next " are taken as-is, even ', except any $ chars
  95. // But you can escape " with a \
  96. var result string
  97. sw.next()
  98. for sw.pos < len(sw.word) {
  99. ch := sw.peek()
  100. if ch == '"' {
  101. sw.next()
  102. break
  103. }
  104. if ch == '$' {
  105. tmp, err := sw.processDollar()
  106. if err != nil {
  107. return "", err
  108. }
  109. result += tmp
  110. } else {
  111. ch = sw.next()
  112. if ch == '\\' {
  113. chNext := sw.peek()
  114. if chNext == '\000' {
  115. // Ignore \ at end of word
  116. continue
  117. }
  118. if chNext == '"' || chNext == '$' {
  119. // \" and \$ can be escaped, all other \'s are left as-is
  120. ch = sw.next()
  121. }
  122. }
  123. result += string(ch)
  124. }
  125. }
  126. return result, nil
  127. }
  128. func (sw *shellWord) processDollar() (string, error) {
  129. sw.next()
  130. ch := sw.peek()
  131. if ch == '{' {
  132. sw.next()
  133. name := sw.processName()
  134. ch = sw.peek()
  135. if ch == '}' {
  136. // Normal ${xx} case
  137. sw.next()
  138. return sw.getEnv(name), nil
  139. }
  140. return "", fmt.Errorf("Unsupported ${} substitution: %s", sw.word)
  141. } else {
  142. // $xxx case
  143. name := sw.processName()
  144. if name == "" {
  145. return "$", nil
  146. }
  147. return sw.getEnv(name), nil
  148. }
  149. }
  150. func (sw *shellWord) processName() string {
  151. // Read in a name (alphanumeric or _)
  152. // If it starts with a numeric then just return $#
  153. var name string
  154. for sw.pos < len(sw.word) {
  155. ch := sw.peek()
  156. if len(name) == 0 && unicode.IsDigit(ch) {
  157. ch = sw.next()
  158. return string(ch)
  159. }
  160. if !unicode.IsLetter(ch) && !unicode.IsDigit(ch) && ch != '_' {
  161. break
  162. }
  163. ch = sw.next()
  164. name += string(ch)
  165. }
  166. return name
  167. }
  168. func (sw *shellWord) getEnv(name string) string {
  169. for _, env := range sw.envs {
  170. i := strings.Index(env, "=")
  171. if i < 0 {
  172. if name == env {
  173. // Should probably never get here, but just in case treat
  174. // it like "var" and "var=" are the same
  175. return ""
  176. }
  177. continue
  178. }
  179. if name != env[:i] {
  180. continue
  181. }
  182. return env[i+1:]
  183. }
  184. return ""
  185. }