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