mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
0ba702c43f
Normalize program names in shellparse.Parse to their basename. Does not rely on filepath.Base because the server may run on either Linux or Windows where the behavior would differ. Closes CODAGT-470
164 lines
4.1 KiB
Go
164 lines
4.1 KiB
Go
package shellparse_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/util/shellparse"
|
|
)
|
|
|
|
func TestParse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want [][]string
|
|
}{
|
|
{
|
|
name: "chained-git-workflow",
|
|
in: `cd /path && git pull && git add . && git commit -m "x"`,
|
|
want: [][]string{{"cd", "/path"}, {"git", "pull"}, {"git", "add"}, {"git", "commit"}},
|
|
},
|
|
{
|
|
name: "single-command-with-flags",
|
|
in: `ls -la /tmp`,
|
|
want: [][]string{{"ls", "/tmp"}},
|
|
},
|
|
{
|
|
name: "no-arg",
|
|
in: `pwd`,
|
|
want: [][]string{{"pwd"}},
|
|
},
|
|
{
|
|
name: "find-xargs-grep-pipeline",
|
|
in: `find /repo -type f | xargs grep "foo" 2>/dev/null | grep -i "bar" | head -30`,
|
|
want: [][]string{{"find", "/repo"}, {"xargs", "grep"}, {"grep", "bar"}, {"head"}},
|
|
},
|
|
{
|
|
name: "stash-build-pop-exit",
|
|
// "ES=$?" is a pure assignment; not a command.
|
|
in: `cd /repo && git stash && go build ./... 2>&1; ES=$?; git stash pop 2>&1 | tail -1; exit $ES`,
|
|
want: [][]string{
|
|
{"cd", "/repo"},
|
|
{"git", "stash"},
|
|
{"go", "build"},
|
|
{"git", "stash"},
|
|
{"tail"},
|
|
{"exit"},
|
|
},
|
|
},
|
|
{
|
|
name: "command-substitution-and-if",
|
|
in: `cd /repo && TOKEN=$(cat /tmp/tok || echo "") && if [ -n "$TOKEN" ]; then echo "$TOKEN" | gh auth login --with-token; else echo "missing"; fi`,
|
|
want: [][]string{
|
|
{"cd", "/repo"},
|
|
{"cat", "/tmp/tok"},
|
|
{"echo"},
|
|
{"[", "]"},
|
|
{"echo"},
|
|
{"gh", "auth"},
|
|
{"echo", "missing"},
|
|
},
|
|
},
|
|
{
|
|
name: "for-loop-with-sed",
|
|
in: `cd /repo && for line in 1 2 3; do
|
|
sed -i "${line}s|a|b|" file
|
|
done`,
|
|
want: [][]string{{"cd", "/repo"}, {"sed", "file"}},
|
|
},
|
|
{
|
|
name: "subshell-and-brace-group",
|
|
in: `(cd /tmp && ls) && { echo a; echo b; }`,
|
|
want: [][]string{{"cd", "/tmp"}, {"ls"}, {"echo", "a"}, {"echo", "b"}},
|
|
},
|
|
{
|
|
name: "variable-program-not-literal",
|
|
in: `$cmd --help && echo done`,
|
|
want: [][]string{{"echo", "done"}},
|
|
},
|
|
{
|
|
name: "double-quoted-positional",
|
|
in: `cd "/repo with spaces"`,
|
|
want: [][]string{{"cd", "/repo with spaces"}},
|
|
},
|
|
{
|
|
name: "single-quoted-positional",
|
|
in: `grep 'fix bug'`,
|
|
want: [][]string{{"grep", "fix bug"}},
|
|
},
|
|
{
|
|
name: "quoted-program-name",
|
|
in: `"/usr/bin/git" pull`,
|
|
want: [][]string{{"git", "pull"}},
|
|
},
|
|
{
|
|
name: "absolute-path-binary",
|
|
in: `/opt/mise/data/installs/go/1.26.2/bin/go test ./...`,
|
|
want: [][]string{{"go", "test"}},
|
|
},
|
|
{
|
|
name: "relative-path-binary",
|
|
in: `./build.sh --verbose`,
|
|
want: [][]string{{"build.sh"}},
|
|
},
|
|
{
|
|
name: "windows-path-binary",
|
|
in: `'C:\Program Files\Go\bin\go.exe' test ./...`,
|
|
want: [][]string{{"go.exe", "test"}},
|
|
},
|
|
{
|
|
name: "double-quoted-with-variable-expansion-skipped",
|
|
in: `echo "hello $name"`,
|
|
// The quoted word contains a parameter expansion, so the
|
|
// parser cannot extract a literal; only the program survives.
|
|
want: [][]string{{"echo"}},
|
|
},
|
|
{
|
|
name: "empty",
|
|
in: ``,
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "comment-only",
|
|
in: `# just a comment`,
|
|
want: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, err := shellparse.Parse(tc.in)
|
|
require.NoError(t, err, "parse failed for %q", tc.in)
|
|
assert.Equal(t, tc.want, got, "input: %q", tc.in)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParse_ParseError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("unterminated-string-no-results", func(t *testing.T) {
|
|
t.Parallel()
|
|
cmds, err := shellparse.Parse(`echo "unterminated`)
|
|
require.Error(t, err)
|
|
require.Nil(t, cmds)
|
|
})
|
|
|
|
t.Run("semicolon-prefix-yields-partial-results-plus-error", func(t *testing.T) {
|
|
t.Parallel()
|
|
// Some malformed inputs (e.g. trailing unterminated tokens after
|
|
// valid semicolon-separated commands) yield partial results
|
|
// alongside a non-nil error. Pin both sides of the contract so
|
|
// future mvdan.cc/sh upgrades that change partial-parse behavior
|
|
// fail this test loudly.
|
|
cmds, err := shellparse.Parse(`ls; cat; echo "unterminated`)
|
|
require.Error(t, err)
|
|
require.Equal(t, [][]string{{"ls"}, {"cat"}}, cmds)
|
|
})
|
|
}
|