add command lists (;, &&, ||) and $?
- split input at unquoted ;, && and || and run the commands like sh;
variables are expanded per command when it runs
- $? and ${?} expand to the exit status of the last command,
exit/quit without argument use it
- syntax errors for missing commands around operators (status 2)
- "command not found" message instead of the raw exec error, no more
"exit status N" message when a command fails
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b602e17622
commit
f5c14dff90
3 files changed
+307
-20
No files matched your search
+124
-1
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -1348,3 +1348,126 @@ func TestReadLine_DoesNotReadAhead(t *testing.T) {
|
||||
t.Errorf("readLine() consumed more than one line, %d bytes left", r.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitList(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want []listItem
|
||||
}{
|
||||
{"", nil},
|
||||
{" ", nil},
|
||||
{"# only a comment", nil},
|
||||
{"ls", []listItem{{"", "ls"}}},
|
||||
{"a; b", []listItem{{"", "a"}, {";", " b"}}},
|
||||
{"a;", []listItem{{"", "a"}}},
|
||||
{"a && b || c", []listItem{{"", "a "}, {"&&", " b "}, {"||", " c"}}},
|
||||
{`echo "a;b" 'c&&d' e\;f`, []listItem{{"", `echo "a;b" 'c&&d' e\;f`}}},
|
||||
{`echo "x\"; y"`, []listItem{{"", `echo "x\"; y"`}}},
|
||||
{"echo ${X:-a;b}; c", []listItem{{"", "echo ${X:-a;b}"}, {";", " c"}}},
|
||||
{"echo a # b; c", []listItem{{"", "echo a "}}},
|
||||
{"echo a#b; c", []listItem{{"", "echo a#b"}, {";", " c"}}},
|
||||
{"echo a|b", []listItem{{"", "echo a|b"}}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got, err := splitList(tc.input)
|
||||
if err != nil {
|
||||
t.Errorf("splitList(%q) returned error: %v", tc.input, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("splitList(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitList_SyntaxErrors(t *testing.T) {
|
||||
for _, input := range []string{"; a", "a ;; b", "&& a", "a &&", "a ||", "a && ; b", "a || && b"} {
|
||||
if _, err := splitList(input); err == nil || !strings.Contains(err.Error(), "syntax error") {
|
||||
t.Errorf("splitList(%q) = %v, want syntax error", input, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecInput_Lists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("D", dir)
|
||||
tests := []struct {
|
||||
input string
|
||||
created []string
|
||||
missing []string
|
||||
status int
|
||||
}{
|
||||
{"touch $D/a; touch $D/b", []string{"a", "b"}, nil, 0},
|
||||
{"false; touch $D/c", []string{"c"}, nil, 0},
|
||||
{"true && touch $D/d", []string{"d"}, nil, 0},
|
||||
{"false && touch $D/e", nil, []string{"e"}, 1},
|
||||
{"false || touch $D/f", []string{"f"}, nil, 0},
|
||||
{"true || touch $D/g", nil, []string{"g"}, 0},
|
||||
{"false && touch $D/h || touch $D/i", []string{"i"}, []string{"h"}, 0},
|
||||
{"true && false || touch $D/j", []string{"j"}, nil, 0},
|
||||
{"gosh-no-such-command || touch $D/k", []string{"k"}, nil, 0},
|
||||
{"true && sh -c 'exit 4'", nil, nil, 4},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
execInput(tc.input)
|
||||
if lastStatus != tc.status {
|
||||
t.Errorf("%q: $? = %d, want %d", tc.input, lastStatus, tc.status)
|
||||
}
|
||||
for _, name := range tc.created {
|
||||
if _, err := os.Stat(filepath.Join(dir, name)); err != nil {
|
||||
t.Errorf("%q: expected %s to be created", tc.input, name)
|
||||
}
|
||||
}
|
||||
for _, name := range tc.missing {
|
||||
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
|
||||
t.Errorf("%q: %s must not be created", tc.input, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecInput_ExpandsPerCommand(t *testing.T) {
|
||||
origDir, _ := os.Getwd()
|
||||
defer os.Chdir(origDir)
|
||||
dir := t.TempDir()
|
||||
out := filepath.Join(dir, "out")
|
||||
t.Setenv("GOSH_TEST_OUT", out)
|
||||
os.Unsetenv("GOSH_TEST_LIST")
|
||||
defer os.Unsetenv("GOSH_TEST_LIST")
|
||||
|
||||
// The variable is set by the first command and used by the second one
|
||||
err := execInput(`set GOSH_TEST_LIST=hello && sh -c 'printf %s "$GOSH_TEST_LIST" > "$GOSH_TEST_OUT"'`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, _ := os.ReadFile(out); string(got) != "hello" {
|
||||
t.Errorf("got %q, want %q", got, "hello")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandVar_LastStatus(t *testing.T) {
|
||||
execInput("sh -c 'exit 3'")
|
||||
for _, input := range []string{"$?", "${?}"} {
|
||||
words, err := splitWords("echo " + input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if words[1] != "3" {
|
||||
t.Errorf("%s = %q, want %q", input, words[1], "3")
|
||||
}
|
||||
}
|
||||
execInput("true")
|
||||
if words, _ := splitWords("echo $?"); words[1] != "0" {
|
||||
t.Errorf("$? = %q after true, want 0", words[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecInput_CommandNotFound(t *testing.T) {
|
||||
err := execInput("gosh-no-such-command")
|
||||
if err == nil || err.Error() != "gosh-no-such-command: command not found" {
|
||||
t.Errorf("got %v, want command not found", err)
|
||||
}
|
||||
if lastStatus != 127 {
|
||||
t.Errorf("$? = %d, want 127", lastStatus)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user