Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
caddy
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
caddy
Commits
94ff7dc6
Commit
94ff7dc6
authored
Oct 27, 2015
by
Matt Holt
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #287 from Makpoc/parsewincmd
Fix windows command parsing
parents
cc229aef
d1b667fb
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
307 additions
and
61 deletions
+307
-61
middleware/commands.go
middleware/commands.go
+98
-5
middleware/commands_test.go
middleware/commands_test.go
+209
-56
No files found.
middleware/commands.go
View file @
94ff7dc6
...
...
@@ -2,18 +2,30 @@ package middleware
import
(
"errors"
"runtime"
"unicode"
"github.com/flynn/go-shlex"
)
var
runtimeGoos
=
runtime
.
GOOS
// SplitCommandAndArgs takes a command string and parses it
// shell-style into the command and its separate arguments.
func
SplitCommandAndArgs
(
command
string
)
(
cmd
string
,
args
[]
string
,
err
error
)
{
parts
,
err
:=
shlex
.
Split
(
command
)
if
err
!=
nil
{
err
=
errors
.
New
(
"error parsing command: "
+
err
.
Error
())
return
}
else
if
len
(
parts
)
==
0
{
var
parts
[]
string
if
runtimeGoos
==
"windows"
{
parts
=
parseWindowsCommand
(
command
)
// parse it Windows-style
}
else
{
parts
,
err
=
parseUnixCommand
(
command
)
// parse it Unix-style
if
err
!=
nil
{
err
=
errors
.
New
(
"error parsing command: "
+
err
.
Error
())
return
}
}
if
len
(
parts
)
==
0
{
err
=
errors
.
New
(
"no command contained in '"
+
command
+
"'"
)
return
}
...
...
@@ -25,3 +37,84 @@ func SplitCommandAndArgs(command string) (cmd string, args []string, err error)
return
}
// parseUnixCommand parses a unix style command line and returns the
// command and its arguments or an error
func
parseUnixCommand
(
cmd
string
)
([]
string
,
error
)
{
return
shlex
.
Split
(
cmd
)
}
// parseWindowsCommand parses windows command lines and
// returns the command and the arguments as an array. It
// should be able to parse commonly used command lines.
// Only basic syntax is supported:
// - spaces in double quotes are not token delimiters
// - double quotes are escaped by either backspace or another double quote
// - except for the above case backspaces are path separators (not special)
//
// Many sources point out that escaping quotes using backslash can be unsafe.
// Use two double quotes when possible. (Source: http://stackoverflow.com/a/31413730/2616179 )
//
// This function has to be used on Windows instead
// of the shlex package because this function treats backslash
// characters properly.
func
parseWindowsCommand
(
cmd
string
)
[]
string
{
const
backslash
=
'\\'
const
quote
=
'"'
var
parts
[]
string
var
part
string
var
inQuotes
bool
var
lastRune
rune
for
i
,
ch
:=
range
cmd
{
if
i
!=
0
{
lastRune
=
rune
(
cmd
[
i
-
1
])
}
if
ch
==
backslash
{
// put it in the part - for now we don't know if it's an
// escaping char or path separator
part
+=
string
(
ch
)
continue
}
if
ch
==
quote
{
if
lastRune
==
backslash
{
// remove the backslash from the part and add the escaped quote instead
part
=
part
[
:
len
(
part
)
-
1
]
part
+=
string
(
ch
)
continue
}
if
lastRune
==
quote
{
// revert the last change of the inQuotes state
// it was an escaping quote
inQuotes
=
!
inQuotes
part
+=
string
(
ch
)
continue
}
// normal escaping quotes
inQuotes
=
!
inQuotes
continue
}
if
unicode
.
IsSpace
(
ch
)
&&
!
inQuotes
&&
len
(
part
)
>
0
{
parts
=
append
(
parts
,
part
)
part
=
""
continue
}
part
+=
string
(
ch
)
}
if
len
(
part
)
>
0
{
parts
=
append
(
parts
,
part
)
part
=
""
}
return
parts
}
middleware/commands_test.go
View file @
94ff7dc6
...
...
@@ -2,11 +2,176 @@ package middleware
import
(
"fmt"
"runtime"
"strings"
"testing"
)
func
TestParseUnixCommand
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
input
string
expected
[]
string
}{
// 0 - emtpy command
{
input
:
``
,
expected
:
[]
string
{},
},
// 1 - command without arguments
{
input
:
`command`
,
expected
:
[]
string
{
`command`
},
},
// 2 - command with single argument
{
input
:
`command arg1`
,
expected
:
[]
string
{
`command`
,
`arg1`
},
},
// 3 - command with multiple arguments
{
input
:
`command arg1 arg2`
,
expected
:
[]
string
{
`command`
,
`arg1`
,
`arg2`
},
},
// 4 - command with single argument with space character - in quotes
{
input
:
`command "arg1 arg1"`
,
expected
:
[]
string
{
`command`
,
`arg1 arg1`
},
},
// 5 - command with multiple spaces and tab character
{
input
:
"command arg1 arg2
\t
arg3"
,
expected
:
[]
string
{
`command`
,
`arg1`
,
`arg2`
,
`arg3`
},
},
// 6 - command with single argument with space character - escaped with backspace
{
input
:
`command arg1\ arg2`
,
expected
:
[]
string
{
`command`
,
`arg1 arg2`
},
},
// 7 - single quotes should escape special chars
{
input
:
`command 'arg1\ arg2'`
,
expected
:
[]
string
{
`command`
,
`arg1\ arg2`
},
},
}
for
i
,
test
:=
range
tests
{
errorPrefix
:=
fmt
.
Sprintf
(
"Test [%d]: "
,
i
)
errorSuffix
:=
fmt
.
Sprintf
(
" Command to parse: [%s]"
,
test
.
input
)
actual
,
_
:=
parseUnixCommand
(
test
.
input
)
if
len
(
actual
)
!=
len
(
test
.
expected
)
{
t
.
Errorf
(
errorPrefix
+
"Expected %d parts, got %d: %#v."
+
errorSuffix
,
len
(
test
.
expected
),
len
(
actual
),
actual
)
continue
}
for
j
:=
0
;
j
<
len
(
actual
);
j
++
{
if
expectedPart
,
actualPart
:=
test
.
expected
[
j
],
actual
[
j
];
expectedPart
!=
actualPart
{
t
.
Errorf
(
errorPrefix
+
"Expected: %v Actual: %v (index %d)."
+
errorSuffix
,
expectedPart
,
actualPart
,
j
)
}
}
}
}
func
TestParseWindowsCommand
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
input
string
expected
[]
string
}{
{
// 0 - empty command - do not fail
input
:
``
,
expected
:
[]
string
{},
},
{
// 1 - cmd without args
input
:
`cmd`
,
expected
:
[]
string
{
`cmd`
},
},
{
// 2 - multiple args
input
:
`cmd arg1 arg2`
,
expected
:
[]
string
{
`cmd`
,
`arg1`
,
`arg2`
},
},
{
// 3 - multiple args with space
input
:
`cmd "combined arg" arg2`
,
expected
:
[]
string
{
`cmd`
,
`combined arg`
,
`arg2`
},
},
{
// 4 - path without spaces
input
:
`mkdir C:\Windows\foo\bar`
,
expected
:
[]
string
{
`mkdir`
,
`C:\Windows\foo\bar`
},
},
{
// 5 - command with space in quotes
input
:
`"command here"`
,
expected
:
[]
string
{
`command here`
},
},
{
// 6 - argument with escaped quotes (two quotes)
input
:
`cmd ""arg""`
,
expected
:
[]
string
{
`cmd`
,
`"arg"`
},
},
{
// 7 - argument with escaped quotes (backslash)
input
:
`cmd \"arg\"`
,
expected
:
[]
string
{
`cmd`
,
`"arg"`
},
},
{
// 8 - two quotes (escaped) inside an inQuote element
input
:
`cmd "a ""quoted value"`
,
expected
:
[]
string
{
`cmd`
,
`a "quoted value`
},
},
// TODO - see how many quotes are dislayed if we use "", """, """""""
{
// 9 - two quotes outside an inQuote element
input
:
`cmd a ""quoted value`
,
expected
:
[]
string
{
`cmd`
,
`a`
,
`"quoted`
,
`value`
},
},
{
// 10 - path with space in quotes
input
:
`mkdir "C:\directory name\foobar"`
,
expected
:
[]
string
{
`mkdir`
,
`C:\directory name\foobar`
},
},
{
// 11 - space without quotes
input
:
`mkdir C:\ space`
,
expected
:
[]
string
{
`mkdir`
,
`C:\`
,
`space`
},
},
{
// 12 - space in quotes
input
:
`mkdir "C:\ space"`
,
expected
:
[]
string
{
`mkdir`
,
`C:\ space`
},
},
{
// 13 - UNC
input
:
`mkdir \\?\C:\Users`
,
expected
:
[]
string
{
`mkdir`
,
`\\?\C:\Users`
},
},
{
// 14 - UNC with space
input
:
`mkdir "\\?\C:\Program Files"`
,
expected
:
[]
string
{
`mkdir`
,
`\\?\C:\Program Files`
},
},
{
// 15 - unclosed quotes - treat as if the path ends with quote
input
:
`mkdir "c:\Program files`
,
expected
:
[]
string
{
`mkdir`
,
`c:\Program files`
},
},
{
// 16 - quotes used inside the argument
input
:
`mkdir "c:\P"rogra"m f"iles`
,
expected
:
[]
string
{
`mkdir`
,
`c:\Program files`
},
},
}
for
i
,
test
:=
range
tests
{
errorPrefix
:=
fmt
.
Sprintf
(
"Test [%d]: "
,
i
)
errorSuffix
:=
fmt
.
Sprintf
(
" Command to parse: [%s]"
,
test
.
input
)
actual
:=
parseWindowsCommand
(
test
.
input
)
if
len
(
actual
)
!=
len
(
test
.
expected
)
{
t
.
Errorf
(
errorPrefix
+
"Expected %d parts, got %d: %#v."
+
errorSuffix
,
len
(
test
.
expected
),
len
(
actual
),
actual
)
continue
}
for
j
:=
0
;
j
<
len
(
actual
);
j
++
{
if
expectedPart
,
actualPart
:=
test
.
expected
[
j
],
actual
[
j
];
expectedPart
!=
actualPart
{
t
.
Errorf
(
errorPrefix
+
"Expected: %v Actual: %v (index %d)."
+
errorSuffix
,
expectedPart
,
actualPart
,
j
)
}
}
}
}
func
TestSplitCommandAndArgs
(
t
*
testing
.
T
)
{
// force linux parsing. It's more robust and covers error cases
runtimeGoos
=
"linux"
defer
func
()
{
runtimeGoos
=
runtime
.
GOOS
}()
var
parseErrorContent
=
"error parsing command:"
var
noCommandErrContent
=
"no command contained in"
...
...
@@ -16,84 +181,42 @@ func TestSplitCommandAndArgs(t *testing.T) {
expectedArgs
[]
string
expectedErrContent
string
}{
//
Test case
0 - emtpy command
// 0 - emtpy command
{
input
:
``
,
expectedCommand
:
``
,
expectedArgs
:
nil
,
expectedErrContent
:
noCommandErrContent
,
},
//
Test case
1 - command without arguments
// 1 - command without arguments
{
input
:
`command`
,
expectedCommand
:
`command`
,
expectedArgs
:
nil
,
expectedErrContent
:
``
,
},
//
Test case
2 - command with single argument
// 2 - command with single argument
{
input
:
`command arg1`
,
expectedCommand
:
`command`
,
expectedArgs
:
[]
string
{
`arg1`
},
expectedErrContent
:
``
,
},
//
Test case
3 - command with multiple arguments
// 3 - command with multiple arguments
{
input
:
`command arg1 arg2`
,
expectedCommand
:
`command`
,
expectedArgs
:
[]
string
{
`arg1`
,
`arg2`
},
expectedErrContent
:
``
,
},
// Test case 4 - command with single argument with space character - in quotes
{
input
:
`command "arg1 arg1"`
,
expectedCommand
:
`command`
,
expectedArgs
:
[]
string
{
`arg1 arg1`
},
expectedErrContent
:
``
,
},
// Test case 4 - command with single argument with space character - escaped
{
input
:
`command arg1\ arg1`
,
expectedCommand
:
`command`
,
expectedArgs
:
[]
string
{
`arg1 arg1`
},
expectedErrContent
:
``
,
},
// Test case 6 - command with escaped quote character
{
input
:
`command "arg1 \" arg1"`
,
expectedCommand
:
`command`
,
expectedArgs
:
[]
string
{
`arg1 " arg1`
},
expectedErrContent
:
``
,
},
// Test case 7 - command with escaped backslash
{
input
:
`command '\arg1'`
,
expectedCommand
:
`command`
,
expectedArgs
:
[]
string
{
`\arg1`
},
expectedErrContent
:
``
,
},
// Test case 8 - command with comments
{
input
:
`command arg1 #comment1 comment2`
,
expectedCommand
:
`command`
,
expectedArgs
:
[]
string
{
`arg1`
},
expectedErrContent
:
""
,
},
// Test case 9 - command with multiple spaces and tab character
{
input
:
"command arg1 arg2
\t
arg3"
,
expectedCommand
:
`command`
,
expectedArgs
:
[]
string
{
`arg1`
,
`arg2`
,
"arg3"
},
expectedErrContent
:
""
,
},
// Test case 10 - command with unclosed quotes
// 4 - command with unclosed quotes
{
input
:
`command "arg1 arg2`
,
expectedCommand
:
""
,
expectedArgs
:
nil
,
expectedErrContent
:
parseErrorContent
,
},
//
Test case 11
- command with unclosed quotes
//
5
- command with unclosed quotes
{
input
:
`command 'arg1 arg2"`
,
expectedCommand
:
""
,
...
...
@@ -120,19 +243,49 @@ func TestSplitCommandAndArgs(t *testing.T) {
// test if command matches
if
test
.
expectedCommand
!=
actualCommand
{
t
.
Errorf
(
"Expected command: [%s], actual: [%s]."
+
errorSuffix
,
test
.
expectedCommand
,
actualCommand
)
t
.
Errorf
(
errorPrefix
+
"Expected command: [%s], actual: [%s]."
+
errorSuffix
,
test
.
expectedCommand
,
actualCommand
)
}
// test if arguments match
if
len
(
test
.
expectedArgs
)
!=
len
(
actualArgs
)
{
t
.
Errorf
(
"Wrong number of arguments! Expected [%v], actual [%v]."
+
errorSuffix
,
test
.
expectedArgs
,
actualArgs
)
}
for
j
,
actualArg
:=
range
actualArgs
{
expectedArg
:=
test
.
expectedArgs
[
j
]
if
actualArg
!=
expectedArg
{
t
.
Errorf
(
errorPrefix
+
"Argument at position [%d] differ! Expected [%s], actual [%s]"
+
errorSuffix
,
j
,
expectedArg
,
actualArg
)
t
.
Errorf
(
errorPrefix
+
"Wrong number of arguments! Expected [%v], actual [%v]."
+
errorSuffix
,
test
.
expectedArgs
,
actualArgs
)
}
else
{
// test args only if the count matches.
for
j
,
actualArg
:=
range
actualArgs
{
expectedArg
:=
test
.
expectedArgs
[
j
]
if
actualArg
!=
expectedArg
{
t
.
Errorf
(
errorPrefix
+
"Argument at position [%d] differ! Expected [%s], actual [%s]"
+
errorSuffix
,
j
,
expectedArg
,
actualArg
)
}
}
}
}
}
func
ExampleSplitCommandAndArgs
()
{
var
commandLine
string
var
command
string
var
args
[]
string
// just for the test - change GOOS and reset it at the end of the test
runtimeGoos
=
"windows"
defer
func
()
{
runtimeGoos
=
runtime
.
GOOS
}()
commandLine
=
`mkdir /P "C:\Program Files"`
command
,
args
,
_
=
SplitCommandAndArgs
(
commandLine
)
fmt
.
Printf
(
"Windows: %s: %s [%s]
\n
"
,
commandLine
,
command
,
strings
.
Join
(
args
,
","
))
// set GOOS to linux
runtimeGoos
=
"linux"
commandLine
=
`mkdir -p /path/with\ space`
command
,
args
,
_
=
SplitCommandAndArgs
(
commandLine
)
fmt
.
Printf
(
"Linux: %s: %s [%s]
\n
"
,
commandLine
,
command
,
strings
.
Join
(
args
,
","
))
// Output:
// Windows: mkdir /P "C:\Program Files": mkdir [/P,C:\Program Files]
// Linux: mkdir -p /path/with\ space: mkdir [-p,/path/with space]
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment