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.

shellquote.go 3.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. // Copyright 2020 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package util
  5. import "strings"
  6. // Bash has the definition of a metacharacter:
  7. // * A character that, when unquoted, separates words.
  8. // A metacharacter is one of: " \t\n|&;()<>"
  9. //
  10. // The following characters also have addition special meaning when unescaped:
  11. // * ‘${[*?!"'`\’
  12. //
  13. // Double Quotes preserve the literal value of all characters with then quotes
  14. // excepting: ‘$’, ‘`’, ‘\’, and, when history expansion is enabled, ‘!’.
  15. // The backslash retains its special meaning only when followed by one of the
  16. // following characters: ‘$’, ‘`’, ‘"’, ‘\’, or newline.
  17. // Backslashes preceding characters without a special meaning are left
  18. // unmodified. A double quote may be quoted within double quotes by preceding
  19. // it with a backslash. If enabled, history expansion will be performed unless
  20. // an ‘!’ appearing in double quotes is escaped using a backslash. The
  21. // backslash preceding the ‘!’ is not removed.
  22. //
  23. // -> This means that `!\n` cannot be safely expressed in `"`.
  24. //
  25. // Looking at the man page for Dash and ash the situation is similar.
  26. //
  27. // Now zsh requires that ‘}’, and ‘]’ are also enclosed in doublequotes or escaped
  28. //
  29. // Single quotes escape everything except a ‘'’
  30. //
  31. // There's one other gotcha - ‘~’ at the start of a string needs to be expanded
  32. // because people always expect that - of course if there is a special character before '/'
  33. // this is not going to work
  34. const (
  35. tildePrefix = '~'
  36. needsEscape = " \t\n|&;()<>${}[]*?!\"'`\\"
  37. needsSingleQuote = "!\n"
  38. )
  39. var doubleQuoteEscaper = strings.NewReplacer(`$`, `\$`, "`", "\\`", `"`, `\"`, `\`, `\\`)
  40. var singleQuoteEscaper = strings.NewReplacer(`'`, `'\''`)
  41. var singleQuoteCoalescer = strings.NewReplacer(`''\'`, `\'`, `\'''`, `\'`)
  42. // ShellEscape will escape the provided string.
  43. // We can't just use go-shellquote here because our preferences for escaping differ from those in that we want:
  44. //
  45. // * If the string doesn't require any escaping just leave it as it is.
  46. // * If the string requires any escaping prefer double quote escaping
  47. // * If we have ! or newlines then we need to use single quote escaping
  48. func ShellEscape(toEscape string) string {
  49. if len(toEscape) == 0 {
  50. return toEscape
  51. }
  52. start := 0
  53. if toEscape[0] == tildePrefix {
  54. // We're in the forcibly non-escaped section...
  55. idx := strings.IndexRune(toEscape, '/')
  56. if idx < 0 {
  57. idx = len(toEscape)
  58. } else {
  59. idx++
  60. }
  61. if !strings.ContainsAny(toEscape[:idx], needsEscape) {
  62. // We'll assume that they intend ~ expansion to occur
  63. start = idx
  64. }
  65. }
  66. // Now for simplicity we'll look at the rest of the string
  67. if !strings.ContainsAny(toEscape[start:], needsEscape) {
  68. return toEscape
  69. }
  70. // OK we have to do some escaping
  71. sb := &strings.Builder{}
  72. _, _ = sb.WriteString(toEscape[:start])
  73. // Do we have any characters which absolutely need to be within single quotes - that is simply ! or \n?
  74. if strings.ContainsAny(toEscape[start:], needsSingleQuote) {
  75. // We need to single quote escape.
  76. sb2 := &strings.Builder{}
  77. _, _ = sb2.WriteRune('\'')
  78. _, _ = singleQuoteEscaper.WriteString(sb2, toEscape[start:])
  79. _, _ = sb2.WriteRune('\'')
  80. _, _ = singleQuoteCoalescer.WriteString(sb, sb2.String())
  81. return sb.String()
  82. }
  83. // OK we can just use " just escape the things that need escaping
  84. _, _ = sb.WriteRune('"')
  85. _, _ = doubleQuoteEscaper.WriteString(sb, toEscape[start:])
  86. _, _ = sb.WriteRune('"')
  87. return sb.String()
  88. }