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