-
Notifications
You must be signed in to change notification settings - Fork 79
/
Copy pathshell_escape.go
167 lines (139 loc) · 4.45 KB
/
shell_escape.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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()
}