-
Notifications
You must be signed in to change notification settings - Fork 60
/
simpleexpand.go
225 lines (207 loc) · 6.04 KB
/
simpleexpand.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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package simpleexpand
import (
"bytes"
"strings"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/syntax"
)
type SimpleExpandContext struct {
HomeDir string
}
type SimpleExpandInfo struct {
HasTilde bool // only ~ as the first character when SimpleExpandContext.HomeDir is set
HasVar bool // $x, $$, ${...}
HasGlob bool // *, ?, [, {
HasExtGlob bool // ?(...) ... ?*+@!
HasHistory bool // ! (anywhere)
HasSpecial bool // subshell, arith
}
func expandHomeDir(info *SimpleExpandInfo, litVal string, multiPart bool, homeDir string) string {
if homeDir == "" {
return litVal
}
if litVal == "~" && !multiPart {
return homeDir
}
if strings.HasPrefix(litVal, "~/") {
info.HasTilde = true
return homeDir + litVal[1:]
}
return litVal
}
func expandLiteral(buf *bytes.Buffer, info *SimpleExpandInfo, litVal string) {
var lastBackSlash bool
var lastExtGlob bool
var lastDollar bool
for _, ch := range litVal {
if ch == 0 {
break
}
if lastBackSlash {
lastBackSlash = false
if ch == '\n' {
// special case, backslash *and* newline are ignored
continue
}
buf.WriteRune(ch)
continue
}
if ch == '\\' {
lastBackSlash = true
lastExtGlob = false
lastDollar = false
continue
}
if ch == '*' || ch == '?' || ch == '[' || ch == '{' {
info.HasGlob = true
}
if ch == '`' {
info.HasSpecial = true
}
if ch == '!' {
info.HasHistory = true
}
if lastExtGlob && ch == '(' {
info.HasExtGlob = true
}
if lastDollar && (ch != ' ' && ch != '"' && ch != '\'' && ch != '(' || ch != '[') {
info.HasVar = true
}
if lastDollar && (ch == '(' || ch == '[') {
info.HasSpecial = true
}
lastExtGlob = (ch == '?' || ch == '*' || ch == '+' || ch == '@' || ch == '!')
lastDollar = (ch == '$')
buf.WriteRune(ch)
}
if lastBackSlash {
buf.WriteByte('\\')
}
}
// also expands ~
func expandLiteralPlus(buf *bytes.Buffer, info *SimpleExpandInfo, litVal string, multiPart bool, ectx SimpleExpandContext) {
litVal = expandHomeDir(info, litVal, multiPart, ectx.HomeDir)
expandLiteral(buf, info, litVal)
}
func expandSQANSILiteral(buf *bytes.Buffer, litVal string) {
// no info specials
if strings.HasSuffix(litVal, "'") {
litVal = litVal[0 : len(litVal)-1]
}
str, _, _ := expand.Format(nil, litVal, nil)
buf.WriteString(str)
}
func expandSQLiteral(buf *bytes.Buffer, litVal string) {
// no info specials
if strings.HasSuffix(litVal, "'") {
litVal = litVal[0 : len(litVal)-1]
}
buf.WriteString(litVal)
}
// will also work for partial double quoted strings
func expandDQLiteral(buf *bytes.Buffer, info *SimpleExpandInfo, litVal string) {
var lastBackSlash bool
var lastDollar bool
for _, ch := range litVal {
if ch == 0 {
break
}
if lastBackSlash {
lastBackSlash = false
if ch == '"' || ch == '\\' || ch == '$' || ch == '`' {
buf.WriteRune(ch)
continue
}
buf.WriteRune('\\')
buf.WriteRune(ch)
continue
}
if ch == '\\' {
lastBackSlash = true
lastDollar = false
continue
}
if ch == '"' {
break
}
// similar to expandLiteral, but no globbing
if ch == '`' {
info.HasSpecial = true
}
if ch == '!' {
info.HasHistory = true
}
if lastDollar && (ch != ' ' && ch != '"' && ch != '\'' && ch != '(' || ch != '[') {
info.HasVar = true
}
if lastDollar && (ch == '(' || ch == '[') {
info.HasSpecial = true
}
lastDollar = (ch == '$')
buf.WriteRune(ch)
}
// in a valid parsed DQ string, you cannot have a trailing backslash (because \" would not end the string)
// still putting the case here though in case we ever deal with incomplete strings (e.g. completion)
if lastBackSlash {
buf.WriteByte('\\')
}
}
func simpleExpandWordInternal(buf *bytes.Buffer, info *SimpleExpandInfo, ectx SimpleExpandContext, parts []syntax.WordPart, sourceStr string, inDoubleQuote bool, level int) {
for partIdx, untypedPart := range parts {
switch part := untypedPart.(type) {
case *syntax.Lit:
if !inDoubleQuote && partIdx == 0 && level == 1 && ectx.HomeDir != "" {
expandLiteralPlus(buf, info, part.Value, len(parts) > 1, ectx)
} else if inDoubleQuote {
expandDQLiteral(buf, info, part.Value)
} else {
expandLiteral(buf, info, part.Value)
}
case *syntax.SglQuoted:
if part.Dollar {
expandSQANSILiteral(buf, part.Value)
} else {
expandSQLiteral(buf, part.Value)
}
case *syntax.DblQuoted:
simpleExpandWordInternal(buf, info, ectx, part.Parts, sourceStr, true, level+1)
default:
rawStr := sourceStr[part.Pos().Offset():part.End().Offset()]
buf.WriteString(rawStr)
}
}
}
// simple word expansion
// expands: literals, single-quoted strings, double-quoted strings (recursively)
// does *not* expand: params (variables), command substitution, arithmetic expressions, process substituions, globs
// for the not expands, they will show up as the literal string
// this is different than expand.Literal which will replace variables as empty string if they aren't defined.
// so "a"'foo'${bar}$x => "afoo${bar}$x", but expand.Literal would produce => "afoo"
// note will do ~ expansion (will not do ~user expansion)
func SimpleExpandWord(ectx SimpleExpandContext, word *syntax.Word, sourceStr string) (string, SimpleExpandInfo) {
var buf bytes.Buffer
var info SimpleExpandInfo
simpleExpandWordInternal(&buf, &info, ectx, word.Parts, sourceStr, false, 1)
return buf.String(), info
}
func SimpleExpandPartialWord(ectx SimpleExpandContext, partialWord string, multiPart bool) (string, SimpleExpandInfo) {
var buf bytes.Buffer
var info SimpleExpandInfo
if partialWord == "" {
return "", info
}
if strings.HasPrefix(partialWord, "\"") {
expandDQLiteral(&buf, &info, partialWord[1:])
} else if strings.HasPrefix(partialWord, "$\"") {
expandDQLiteral(&buf, &info, partialWord[2:])
} else if strings.HasPrefix(partialWord, "'") {
expandSQLiteral(&buf, partialWord[1:])
} else if strings.HasPrefix(partialWord, "$'") {
expandSQANSILiteral(&buf, partialWord[2:])
} else {
expandLiteralPlus(&buf, &info, partialWord, multiPart, ectx)
}
return buf.String(), info
}