Skip to content

Commit 866d8eb

Browse files
committed
Fixes #44
1 parent 9e822f2 commit 866d8eb

File tree

2 files changed

+135
-34
lines changed

2 files changed

+135
-34
lines changed

shellwords.go

+105-28
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package shellwords
22

33
import (
4+
"bytes"
45
"errors"
56
"os"
67
"regexp"
78
"strings"
9+
"unicode"
810
)
911

1012
var (
@@ -27,13 +29,72 @@ func replaceEnv(getenv func(string) string, s string) string {
2729
getenv = os.Getenv
2830
}
2931

30-
return envRe.ReplaceAllStringFunc(s, func(s string) string {
31-
s = s[1:]
32-
if s[0] == '{' {
33-
s = s[1 : len(s)-1]
32+
var buf bytes.Buffer
33+
rs := []rune(s)
34+
for i := 0; i < len(rs); i++ {
35+
r := rs[i]
36+
if r == '\\' {
37+
i++
38+
if i == len(rs) {
39+
break
40+
}
41+
buf.WriteRune(rs[i])
42+
continue
43+
} else if r == '$' {
44+
i++
45+
if i == len(rs) {
46+
buf.WriteRune(r)
47+
break
48+
}
49+
if rs[i] == 0x7b {
50+
i++
51+
p := i
52+
for ; i < len(rs); i++ {
53+
r = rs[i]
54+
if r == '\\' {
55+
i++
56+
if i == len(rs) {
57+
return s
58+
}
59+
continue
60+
}
61+
if r == 0x7d || (!unicode.IsLetter(r) && r != '_' && !unicode.IsDigit(r)) {
62+
break
63+
}
64+
}
65+
if r != 0x7d {
66+
return s
67+
}
68+
if i > p {
69+
buf.WriteString(getenv(s[p:i]))
70+
}
71+
} else {
72+
p := i
73+
for ; i < len(rs); i++ {
74+
r := rs[i]
75+
if r == '\\' {
76+
i++
77+
if i == len(rs) {
78+
return s
79+
}
80+
continue
81+
}
82+
if !unicode.IsLetter(r) && r != '_' && !unicode.IsDigit(r) {
83+
break
84+
}
85+
}
86+
if i > p {
87+
buf.WriteString(getenv(s[p:i]))
88+
i--
89+
} else {
90+
buf.WriteString(s[p:])
91+
}
92+
}
93+
} else {
94+
buf.WriteRune(r)
3495
}
35-
return getenv(s)
36-
})
96+
}
97+
return buf.String()
3798
}
3899

39100
type Parser struct {
@@ -56,14 +117,22 @@ func NewParser() *Parser {
56117
}
57118
}
58119

120+
type argType int
121+
122+
const (
123+
argNo argType = iota
124+
argSingle
125+
argQuoted
126+
)
127+
59128
func (p *Parser) Parse(line string) ([]string, error) {
60129
args := []string{}
61130
buf := ""
62131
var escaped, doubleQuoted, singleQuoted, backQuote, dollarQuote bool
63132
backtick := ""
64133

65134
pos := -1
66-
got := false
135+
got := argNo
67136

68137
i := -1
69138
loop:
@@ -89,21 +158,25 @@ loop:
89158
if singleQuoted || doubleQuoted || backQuote || dollarQuote {
90159
buf += string(r)
91160
backtick += string(r)
92-
} else if got {
161+
} else if got != argNo {
93162
if p.ParseEnv {
94-
parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir}
95-
strs, err := parser.Parse(replaceEnv(p.Getenv, buf))
96-
if err != nil {
97-
return nil, err
98-
}
99-
for _, str := range strs {
100-
args = append(args, str)
163+
if got == argSingle {
164+
parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir}
165+
strs, err := parser.Parse(replaceEnv(p.Getenv, buf))
166+
if err != nil {
167+
return nil, err
168+
}
169+
for _, str := range strs {
170+
args = append(args, str)
171+
}
172+
} else {
173+
args = append(args, replaceEnv(p.Getenv, buf))
101174
}
102175
} else {
103176
args = append(args, buf)
104177
}
105178
buf = ""
106-
got = false
179+
got = argNo
107180
}
108181
continue
109182
}
@@ -156,15 +229,15 @@ loop:
156229
case '"':
157230
if !singleQuoted && !dollarQuote {
158231
if doubleQuoted {
159-
got = true
232+
got = argQuoted
160233
}
161234
doubleQuoted = !doubleQuoted
162235
continue
163236
}
164237
case '\'':
165238
if !doubleQuoted && !dollarQuote {
166239
if singleQuoted {
167-
got = true
240+
got = argSingle
168241
}
169242
singleQuoted = !singleQuoted
170243
continue
@@ -174,30 +247,34 @@ loop:
174247
if r == '>' && len(buf) > 0 {
175248
if c := buf[0]; '0' <= c && c <= '9' {
176249
i -= 1
177-
got = false
250+
got = argNo
178251
}
179252
}
180253
pos = i
181254
break loop
182255
}
183256
}
184257

185-
got = true
258+
got = argSingle
186259
buf += string(r)
187260
if backQuote || dollarQuote {
188261
backtick += string(r)
189262
}
190263
}
191264

192-
if got {
265+
if got != argNo {
193266
if p.ParseEnv {
194-
parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir}
195-
strs, err := parser.Parse(replaceEnv(p.Getenv, buf))
196-
if err != nil {
197-
return nil, err
198-
}
199-
for _, str := range strs {
200-
args = append(args, str)
267+
if got == argSingle {
268+
parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir}
269+
strs, err := parser.Parse(replaceEnv(p.Getenv, buf))
270+
if err != nil {
271+
return nil, err
272+
}
273+
for _, str := range strs {
274+
args = append(args, str)
275+
}
276+
} else {
277+
args = append(args, replaceEnv(p.Getenv, buf))
201278
}
202279
} else {
203280
args = append(args, buf)

shellwords_test.go

+30-6
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,9 @@ func TestEnvArgumentsFail(t *testing.T) {
288288
t.Fatal("Should be an error")
289289
}
290290
os.Setenv("FOO", "bar `")
291-
_, err = parser.Parse("$FOO ")
291+
result, err := parser.Parse("$FOO ")
292292
if err == nil {
293-
t.Fatal("Should be an error")
293+
t.Fatal("Should be an error: ", result)
294294
}
295295
}
296296

@@ -300,20 +300,20 @@ func TestDupEnv(t *testing.T) {
300300

301301
parser := NewParser()
302302
parser.ParseEnv = true
303-
args, err := parser.Parse("echo $$FOO$")
303+
args, err := parser.Parse("echo $FOO$")
304304
if err != nil {
305305
t.Fatal(err)
306306
}
307-
expected := []string{"echo", "$bar$"}
307+
expected := []string{"echo", "bar$"}
308308
if !reflect.DeepEqual(args, expected) {
309309
t.Fatalf("Expected %#v, but %#v:", expected, args)
310310
}
311311

312-
args, err = parser.Parse("echo $${FOO_BAR}$")
312+
args, err = parser.Parse("echo ${FOO_BAR}$")
313313
if err != nil {
314314
t.Fatal(err)
315315
}
316-
expected = []string{"echo", "$baz$"}
316+
expected = []string{"echo", "baz$"}
317317
if !reflect.DeepEqual(args, expected) {
318318
t.Fatalf("Expected %#v, but %#v:", expected, args)
319319
}
@@ -383,3 +383,27 @@ func TestBackquoteInFlag(t *testing.T) {
383383
t.Fatalf("Expected %#v, but %#v:", expected, args)
384384
}
385385
}
386+
387+
func TestEnvInQuoted(t *testing.T) {
388+
os.Setenv("FOO", "bar")
389+
390+
parser := NewParser()
391+
parser.ParseEnv = true
392+
args, err := parser.Parse(`ssh 127.0.0.1 "echo $FOO"`)
393+
if err != nil {
394+
panic(err)
395+
}
396+
expected := []string{"ssh", "127.0.0.1", "echo bar"}
397+
if !reflect.DeepEqual(args, expected) {
398+
t.Fatalf("Expected %#v, but %#v:", expected, args)
399+
}
400+
401+
args, err = parser.Parse(`ssh 127.0.0.1 "echo \\$FOO"`)
402+
if err != nil {
403+
panic(err)
404+
}
405+
expected = []string{"ssh", "127.0.0.1", "echo $FOO"}
406+
if !reflect.DeepEqual(args, expected) {
407+
t.Fatalf("Expected %#v, but %#v:", expected, args)
408+
}
409+
}

0 commit comments

Comments
 (0)