forked from TrueCloudLab/restic
sftp: Add SplitShellArgs
This commit is contained in:
parent
d1efdcd78e
commit
d3b6f75848
2 changed files with 178 additions and 0 deletions
73
src/restic/backend/sftp/split.go
Normal file
73
src/restic/backend/sftp/split.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = `"foo" "bar" baz "test argument" another 'test arg' "last \" argument" 'another \" last argument'`
|
||||||
|
|
||||||
|
// shellSplitter splits a command string into separater arguments. It supports
|
||||||
|
// single and double quoted strings.
|
||||||
|
type shellSplitter struct {
|
||||||
|
quote rune
|
||||||
|
lastChar rune
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *shellSplitter) isSplitChar(c rune) bool {
|
||||||
|
// only test for quotes if the last char was not a backslash
|
||||||
|
if s.lastChar != '\\' {
|
||||||
|
|
||||||
|
// quote ended
|
||||||
|
if s.quote != 0 && c == s.quote {
|
||||||
|
s.quote = 0
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// quote starts
|
||||||
|
if s.quote == 0 && (c == '"' || c == '\'') {
|
||||||
|
s.quote = c
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastChar = c
|
||||||
|
|
||||||
|
// within quote
|
||||||
|
if s.quote != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// outside quote
|
||||||
|
return c == '\\' || unicode.IsSpace(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitShellArgs returns the list of arguments from a shell command string.
|
||||||
|
func SplitShellArgs(data string) (list []string, err error) {
|
||||||
|
s := &shellSplitter{}
|
||||||
|
|
||||||
|
// derived from strings.SplitFunc
|
||||||
|
fieldStart := -1 // Set to -1 when looking for start of field.
|
||||||
|
for i, rune := range data {
|
||||||
|
if s.isSplitChar(rune) {
|
||||||
|
if fieldStart >= 0 {
|
||||||
|
list = append(list, data[fieldStart:i])
|
||||||
|
fieldStart = -1
|
||||||
|
}
|
||||||
|
} else if fieldStart == -1 {
|
||||||
|
fieldStart = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fieldStart >= 0 { // Last field might end at EOF.
|
||||||
|
list = append(list, data[fieldStart:])
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s.quote {
|
||||||
|
case '\'':
|
||||||
|
return nil, errors.New("single-quoted string not terminated")
|
||||||
|
case '"':
|
||||||
|
return nil, errors.New("double-quoted string not terminated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
105
src/restic/backend/sftp/split_test.go
Normal file
105
src/restic/backend/sftp/split_test.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShellSplitter(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
data string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`foo`,
|
||||||
|
[]string{"foo"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`'foo'`,
|
||||||
|
[]string{"foo"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo bar baz`,
|
||||||
|
[]string{"foo", "bar", "baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo 'bar' baz`,
|
||||||
|
[]string{"foo", "bar", "baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo 'bar box' baz`,
|
||||||
|
[]string{"foo", "bar box", "baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`"bar 'box'" baz`,
|
||||||
|
[]string{"bar 'box'", "baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`'bar "box"' baz`,
|
||||||
|
[]string{`bar "box"`, "baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`\"bar box baz`,
|
||||||
|
[]string{`"bar`, "box", "baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`"bar/foo/x" "box baz"`,
|
||||||
|
[]string{"bar/foo/x", "box baz"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
res, err := SplitShellArgs(test.data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(res, test.want) {
|
||||||
|
t.Fatalf("wrong data returned, want:\n %#v\ngot:\n %#v",
|
||||||
|
test.want, res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellSplitterInvalid(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
data string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"foo'",
|
||||||
|
"single-quoted string not terminated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo"`,
|
||||||
|
"double-quoted string not terminated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"foo 'bar",
|
||||||
|
"single-quoted string not terminated",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`foo "bar`,
|
||||||
|
"double-quoted string not terminated",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
res, err := SplitShellArgs(test.data)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error not found: %v", test.err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Error() != test.err {
|
||||||
|
t.Fatalf("expected error not found, want:\n %q\ngot:\n %q", test.err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) > 0 {
|
||||||
|
t.Fatalf("splitter returned fields from invalid data: %v", res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue