You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

102 lines
2.3 KiB

  1. package shellquote
  2. import (
  3. "bytes"
  4. "strings"
  5. "unicode/utf8"
  6. )
  7. // Join quotes each argument and joins them with a space.
  8. // If passed to /bin/sh, the resulting string will be split back into the
  9. // original arguments.
  10. func Join(args ...string) string {
  11. var buf bytes.Buffer
  12. for i, arg := range args {
  13. if i != 0 {
  14. buf.WriteByte(' ')
  15. }
  16. quote(arg, &buf)
  17. }
  18. return buf.String()
  19. }
  20. const (
  21. specialChars = "\\'\"`${[|&;<>()*?!"
  22. extraSpecialChars = " \t\n"
  23. prefixChars = "~"
  24. )
  25. func quote(word string, buf *bytes.Buffer) {
  26. // We want to try to produce a "nice" output. As such, we will
  27. // backslash-escape most characters, but if we encounter a space, or if we
  28. // encounter an extra-special char (which doesn't work with
  29. // backslash-escaping) we switch over to quoting the whole word. We do this
  30. // with a space because it's typically easier for people to read multi-word
  31. // arguments when quoted with a space rather than with ugly backslashes
  32. // everywhere.
  33. origLen := buf.Len()
  34. if len(word) == 0 {
  35. // oops, no content
  36. buf.WriteString("''")
  37. return
  38. }
  39. cur, prev := word, word
  40. atStart := true
  41. for len(cur) > 0 {
  42. c, l := utf8.DecodeRuneInString(cur)
  43. cur = cur[l:]
  44. if strings.ContainsRune(specialChars, c) || (atStart && strings.ContainsRune(prefixChars, c)) {
  45. // copy the non-special chars up to this point
  46. if len(cur) < len(prev) {
  47. buf.WriteString(prev[0 : len(prev)-len(cur)-l])
  48. }
  49. buf.WriteByte('\\')
  50. buf.WriteRune(c)
  51. prev = cur
  52. } else if strings.ContainsRune(extraSpecialChars, c) {
  53. // start over in quote mode
  54. buf.Truncate(origLen)
  55. goto quote
  56. }
  57. atStart = false
  58. }
  59. if len(prev) > 0 {
  60. buf.WriteString(prev)
  61. }
  62. return
  63. quote:
  64. // quote mode
  65. // Use single-quotes, but if we find a single-quote in the word, we need
  66. // to terminate the string, emit an escaped quote, and start the string up
  67. // again
  68. inQuote := false
  69. for len(word) > 0 {
  70. i := strings.IndexRune(word, '\'')
  71. if i == -1 {
  72. break
  73. }
  74. if i > 0 {
  75. if !inQuote {
  76. buf.WriteByte('\'')
  77. inQuote = true
  78. }
  79. buf.WriteString(word[0:i])
  80. }
  81. word = word[i+1:]
  82. if inQuote {
  83. buf.WriteByte('\'')
  84. inQuote = false
  85. }
  86. buf.WriteString("\\'")
  87. }
  88. if len(word) > 0 {
  89. if !inQuote {
  90. buf.WriteByte('\'')
  91. }
  92. buf.WriteString(word)
  93. buf.WriteByte('\'')
  94. }
  95. }