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.5KB

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