helpers/shell_escape.go (119 lines of code) (raw):
package helpers
import (
"fmt"
"regexp"
"sort"
"strings"
)
type mode string
const (
lit mode = "literal"
quo mode = "quote"
hextable = "0123456789abcdef"
)
// modeTable is a mapping of ascii characters to an escape mode:
// - escape character: where the mode is also the escaped string
// - literal: a string full of only literals does not require quoting
// - quote: a character that will need string quoting
// - "": a missing mapping indicates that the character will need hex quoting
//
// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
var modeTable = [256]mode{
'\a': `\a`, '\b': `\b`, '\t': `\t`, '\n': `\n`, '\v': `\v`, '\f': `\f`,
'\r': `\r`, '\'': `\'`, '\\': `\\`,
',': lit, '-': lit, '.': lit, '/': lit,
'0': lit, '1': lit, '2': lit, '3': lit, '4': lit, '5': lit, '6': lit,
'7': lit, '8': lit, '9': lit,
'@': lit, 'A': lit, 'B': lit, 'C': lit, 'D': lit, 'E': lit, 'F': lit,
'G': lit, 'H': lit, 'I': lit, 'J': lit, 'K': lit, 'L': lit, 'M': lit,
'N': lit, 'O': lit, 'P': lit, 'Q': lit, 'R': lit, 'S': lit, 'T': lit,
'U': lit, 'V': lit, 'W': lit, 'X': lit, 'Y': lit, 'Z': lit,
'_': lit, 'a': lit, 'b': lit, 'c': lit, 'd': lit, 'e': lit, 'f': lit,
'g': lit, 'h': lit, 'i': lit, 'j': lit, 'k': lit, 'l': lit, 'm': lit,
'n': lit, 'o': lit, 'p': lit, 'q': lit, 'r': lit, 's': lit, 't': lit,
'u': lit, 'v': lit, 'w': lit, 'x': lit, 'y': lit, 'z': lit,
' ': quo, '!': quo, '"': quo, '#': quo, '$': quo, '%': quo, '&': quo,
'(': quo, ')': quo, '*': quo, '+': quo, ':': quo, ';': quo, '<': quo,
'=': quo, '>': quo, '?': quo, '[': quo, ']': quo, '^': quo, '`': quo,
'{': quo, '|': quo, '}': quo, '~': quo,
}
// ShellEscape returns either a string identical to the input, or an escaped
// string if certain characters are present. ANSI-C Quoting is used for
// control characters and hexcodes are used for non-ascii characters.
func ShellEscape(input string) string {
if input == "" {
return "''"
}
var sb strings.Builder
sb.Grow(len(input) * 2)
escape := false
for _, c := range []byte(input) {
mode := modeTable[c]
switch mode {
case lit:
sb.WriteByte(c)
case quo:
sb.WriteByte(c)
escape = true
case "":
sb.Write([]byte{'\\', 'x', hextable[c>>4], hextable[c&0x0f]})
escape = true
default:
sb.WriteString(string(mode))
escape = true
}
}
if escape {
return "$'" + sb.String() + "'"
}
return sb.String()
}
// posixModeTable defines what characters need quoting, and which need to be
// backslash escaped:
//
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_02
var posixModeTable = [256]mode{
'`': "\\`", '"': `\"`, '\\': `\\`, '$': `\$`,
' ': quo, '!': quo, '#': quo, '%': quo, '&': quo, '(': quo, ')': quo,
'*': quo, '<': quo, '=': quo, '>': quo, '?': quo, '[': quo, '|': quo,
}
// PosixShellEscape double quotes strings and escapes a string where necessary.
func PosixShellEscape(input string) string {
if input == "" {
return "''"
}
var sb strings.Builder
sb.Grow(len(input) * 2)
escape := false
for _, c := range []byte(input) {
mode := posixModeTable[c]
switch mode {
case quo:
sb.WriteByte(c)
escape = true
case "":
sb.WriteByte(c)
default:
sb.WriteString(string(mode))
escape = true
}
}
if escape {
return `"` + sb.String() + `"`
}
return sb.String()
}
// isValidDotEnvKey checks if a key is valid for a .env file
// (alphanumeric or underscores, starting with a letter or underscore).
func isValidDotEnvKey(key string) bool {
validKeyPattern := `^[A-Za-z_][A-Za-z0-9_]*$`
matched, _ := regexp.MatchString(validKeyPattern, key)
return matched
}
// The gotdotenv parser unescapes newlines and other characters:
// https://github.com/joho/godotenv/blob/3a7a19020151b45a29896c9142723efe5b11a061/parser.go#L193-L206
// Note that \t is not on the list.
var escapeDotEnvValue = strings.NewReplacer(
"\\", "\\\\", // Escape backslashes
"\"", "\\\"", // Escape double quotes
"\n", "\\n", // Escape newlines
"\r", "\\r", // Escape carriage returns
).Replace
func DotEnvEscape(variables map[string]string) string {
var sb strings.Builder
// Sort variables to get deterministic output
keys := make([]string, 0, len(variables))
for key := range variables {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if !isValidDotEnvKey(key) {
// Skip invalid keys
continue
}
value := variables[key]
sb.WriteString(fmt.Sprintf("%s=\"%s\"\n", key, escapeDotEnvValue(value)))
}
return sb.String()
}