unquote.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. package shellquote
  2. import (
  3. "bytes"
  4. "errors"
  5. "strings"
  6. "unicode/utf8"
  7. )
  8. var (
  9. UnterminatedSingleQuoteError = errors.New("Unterminated single-quoted string")
  10. UnterminatedDoubleQuoteError = errors.New("Unterminated double-quoted string")
  11. UnterminatedEscapeError = errors.New("Unterminated backslash-escape")
  12. )
  13. var (
  14. splitChars = " \n\t"
  15. singleChar = '\''
  16. doubleChar = '"'
  17. escapeChar = '\\'
  18. doubleEscapeChars = "$`\"\n\\"
  19. )
  20. // Split splits a string according to /bin/sh's word-splitting rules. It
  21. // supports backslash-escapes, single-quotes, and double-quotes. Notably it does
  22. // not support the $'' style of quoting. It also doesn't attempt to perform any
  23. // other sort of expansion, including brace expansion, shell expansion, or
  24. // pathname expansion.
  25. //
  26. // If the given input has an unterminated quoted string or ends in a
  27. // backslash-escape, one of UnterminatedSingleQuoteError,
  28. // UnterminatedDoubleQuoteError, or UnterminatedEscapeError is returned.
  29. func Split(input string) (words []string, err error) {
  30. var buf bytes.Buffer
  31. words = make([]string, 0)
  32. for len(input) > 0 {
  33. // skip any splitChars at the start
  34. c, l := utf8.DecodeRuneInString(input)
  35. if strings.ContainsRune(splitChars, c) {
  36. input = input[l:]
  37. continue
  38. } else if c == escapeChar {
  39. // Look ahead for escaped newline so we can skip over it
  40. next := input[l:]
  41. if len(next) == 0 {
  42. err = UnterminatedEscapeError
  43. return
  44. }
  45. c2, l2 := utf8.DecodeRuneInString(next)
  46. if c2 == '\n' {
  47. input = next[l2:]
  48. continue
  49. }
  50. }
  51. var word string
  52. word, input, err = splitWord(input, &buf)
  53. if err != nil {
  54. return
  55. }
  56. words = append(words, word)
  57. }
  58. return
  59. }
  60. func splitWord(input string, buf *bytes.Buffer) (word string, remainder string, err error) {
  61. buf.Reset()
  62. raw:
  63. {
  64. cur := input
  65. for len(cur) > 0 {
  66. c, l := utf8.DecodeRuneInString(cur)
  67. cur = cur[l:]
  68. if c == singleChar {
  69. buf.WriteString(input[0 : len(input)-len(cur)-l])
  70. input = cur
  71. goto single
  72. } else if c == doubleChar {
  73. buf.WriteString(input[0 : len(input)-len(cur)-l])
  74. input = cur
  75. goto double
  76. } else if c == escapeChar {
  77. buf.WriteString(input[0 : len(input)-len(cur)-l])
  78. input = cur
  79. goto escape
  80. } else if strings.ContainsRune(splitChars, c) {
  81. buf.WriteString(input[0 : len(input)-len(cur)-l])
  82. return buf.String(), cur, nil
  83. }
  84. }
  85. if len(input) > 0 {
  86. buf.WriteString(input)
  87. input = ""
  88. }
  89. goto done
  90. }
  91. escape:
  92. {
  93. if len(input) == 0 {
  94. return "", "", UnterminatedEscapeError
  95. }
  96. c, l := utf8.DecodeRuneInString(input)
  97. if c == '\n' {
  98. // a backslash-escaped newline is elided from the output entirely
  99. } else {
  100. buf.WriteString(input[:l])
  101. }
  102. input = input[l:]
  103. }
  104. goto raw
  105. single:
  106. {
  107. i := strings.IndexRune(input, singleChar)
  108. if i == -1 {
  109. return "", "", UnterminatedSingleQuoteError
  110. }
  111. buf.WriteString(input[0:i])
  112. input = input[i+1:]
  113. goto raw
  114. }
  115. double:
  116. {
  117. cur := input
  118. for len(cur) > 0 {
  119. c, l := utf8.DecodeRuneInString(cur)
  120. cur = cur[l:]
  121. if c == doubleChar {
  122. buf.WriteString(input[0 : len(input)-len(cur)-l])
  123. input = cur
  124. goto raw
  125. } else if c == escapeChar {
  126. // bash only supports certain escapes in double-quoted strings
  127. c2, l2 := utf8.DecodeRuneInString(cur)
  128. cur = cur[l2:]
  129. if strings.ContainsRune(doubleEscapeChars, c2) {
  130. buf.WriteString(input[0 : len(input)-len(cur)-l-l2])
  131. if c2 == '\n' {
  132. // newline is special, skip the backslash entirely
  133. } else {
  134. buf.WriteRune(c2)
  135. }
  136. input = cur
  137. }
  138. }
  139. }
  140. return "", "", UnterminatedDoubleQuoteError
  141. }
  142. done:
  143. return buf.String(), input, nil
  144. }