package main import ( "os" "os/exec" "strings" "testing" ) func TestExpandParam(t *testing.T) { resetShell(t) t.Setenv("P", "/usr/local/lib/libfoo.so.1") t.Setenv("F", "archive.tar.gz") t.Setenv("H", "hello") t.Setenv("E", "") t.Setenv("U8", "Grüße") t.Setenv("SP", "a b c") t.Setenv("GLOB", "*.go") os.Unsetenv("UNSET") positional = []string{"one", "two"} tests := []struct { input string want string }{ {"${#P}", "26"}, {"${#U8}", "5"}, {"${#1}", "3"}, {"${#}", "2"}, {"${P#*/}", "usr/local/lib/libfoo.so.1"}, {"${P##*/}", "libfoo.so.1"}, {"${P%/*}", "/usr/local/lib"}, {"${P%%.*}", "/usr/local/lib/libfoo"}, {"${F%.gz}", "archive.tar"}, {"${F#*.}", "tar.gz"}, {"${F##*.}", "gz"}, {"${F%.zip}", "archive.tar.gz"}, {"${U8%e}", "Grüß"}, {"${U8#?}", "rüße"}, {"${H/l/L}", "heLlo"}, {"${H//l/L}", "heLLo"}, {"${H//l}", "heo"}, {"${H/#h/H}", "Hello"}, {"${H/#e/E}", "hello"}, {"${H/%o/O}", "hellO"}, {"${H/l*/X}", "heX"}, {"${SP// /_}", "a_b__c"}, {"${H/[aeiou]/_}", "h_llo"}, {"${H:1}", "ello"}, {"${H:1:3}", "ell"}, {"${H: -2}", "lo"}, {"${H:0:-1}", "hell"}, {"${H:1+1:2*1}", "ll"}, {"${H:10}", ""}, {"${U8:1:3}", "rüß"}, {"${UNSET-dflt}", "dflt"}, {"${UNSET:-dflt}", "dflt"}, {"${E-dflt}", ""}, {"${E:-dflt}", "dflt"}, {"${H:-dflt}", "hello"}, {"${UNSET+alt}", ""}, {"${E+alt}", "alt"}, {"${E:+alt}", ""}, {"${H:+alt}", "alt"}, {"${UNSET:-${H}}", "hello"}, {"${UNSET:-${UNSET:-deep}}", "deep"}, {`${UNSET:-"a b"}`, "a b"}, {"${UNSET:-a b}", "a b"}, {"${UNSET:-$(echo sub)}", "sub"}, {"${UNSET:-$((1+2))}", "3"}, {"${UNSET:-}", ""}, {"${2:-x}", "two"}, {"${3:-three}", "three"}, // Quoted characters and values of variables are no patterns {"${GLOB#*.}", "go"}, {`${GLOB#"*"}`, ".go"}, {"${GLOB%$GLOB}", ""}, {"${F%$GLOB}", "archive.tar.gz"}, {`${P//\//:}`, ":usr:local:lib:libfoo.so.1"}, } for _, tc := range tests { got, err := splitWords(`"` + tc.input + `"`) if err != nil { t.Errorf("%s: %v", tc.input, err) continue } if len(got) != 1 || got[0] != tc.want { t.Errorf("%s = %q, want %q", tc.input, got, tc.want) } } // := assigns defer os.Unsetenv("GOSH_ASSIGN") os.Unsetenv("GOSH_ASSIGN") if got, _ := splitWords("${GOSH_ASSIGN:=set} $GOSH_ASSIGN"); strings.Join(got, " ") != "set set" { t.Errorf(":= gives %q", got) } for input, want := range map[string]string{ "${UNSET}": "UNSET: unbound variable", "${#UNSET}": "UNSET: unbound variable", "${UNSET%x}": "UNSET: unbound variable", "${UNSET:?is required}": "UNSET: is required", "${E:?}": "E: parameter null or not set", "${1x}": "bad substitution: ${1x}", "${H!}": "bad substitution: ${H!}", "${3:=x}": "$3: cannot assign in this way", "${H:3:-5}": "-5: substring expression < 0", "${H": "missing } in variable reference", } { if _, err := splitWords(input); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: err = %v, want %q", input, err, want) } } } func TestArith(t *testing.T) { t.Setenv("N", "5") t.Setenv("EXPR", "N*2") t.Setenv("EMPTY", "") os.Unsetenv("UNSET") tests := []struct { expr string want int64 }{ {"1+2*3", 7}, {"(1+2)*3", 9}, {"7/2", 3}, {"-7/2", -3}, {"7%3", 1}, {"2**10", 1024}, {"-2**2", 4}, {"2**3**2", 512}, {"1<<4", 16}, {"256>>4", 16}, {"~0", -1}, {"!0", 1}, {"!5", 0}, {"3 > 2 && 2 > 1", 1}, {"0 || 0", 0}, {"5 & 3", 1}, {"5 | 3", 7}, {"5 ^ 3", 6}, {"1 ? 10 : 20", 10}, {"0 ? 10 : 20", 20}, {"0x1F", 31}, {"010", 8}, {"N", 5}, {"N * N", 25}, {"EXPR + 1", 11}, {"EMPTY + 1", 1}, {"1, 2, 3", 3}, {"", 0}, {" 42 ", 42}, // Short-circuit: no error and no assignment on the other side {"0 && 1/0", 0}, {"1 || UNSET", 1}, {"1 ? 2 : 1/0", 2}, } for _, tc := range tests { got, err := evalArith(tc.expr, 0) if err != nil || got != tc.want { t.Errorf("$((%s)) = %d, %v, want %d", tc.expr, got, err, tc.want) } } for expr, want := range map[string]string{ "1/0": "division by 0", "1%0": "division by 0", "2**-1": "exponent less than 0", "UNSET+1": "UNSET: unbound variable", "1 +": "arithmetic syntax error", "(1": "arithmetic syntax error", "1 2": "arithmetic syntax error", "1 @ 2": "invalid arithmetic operator", "09": "value too great for base", "5 = 3": "arithmetic syntax error", } { if _, err := evalArith(expr, 0); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("$((%s)): err = %v, want %q", expr, err, want) } } } func TestExec_Arith(t *testing.T) { resetShell(t) for _, name := range []string{"I", "J"} { os.Unsetenv(name) defer os.Unsetenv(name) } tests := []struct { input string want string }{ {"echo $((1 + 2)) > out", "3\n"}, {"I=5; echo $((I + 1)) $(($I * 2)) > out", "6 10\n"}, {"I=5; echo $((I++)) $I $((++I)) $((I += 10)) $I > out", "5 6 7 17 17\n"}, {"I=1; : $((I = I * 3)); echo $I > out", "3\n"}, {"I=0; while [ $I -lt 3 ]; do I=$((I + 1)); done; echo $I > out", "3\n"}, {"echo $(( $(echo 3) + ${J:-4} )) > out", "7\n"}, {`echo "$((2 * 21))" > out`, "42\n"}, {"echo x$((1+1))y > out", "x2y\n"}, {"set -- 3 4; echo $(($1 * $2)) $(($# + 1)) > out", "12 3\n"}, // $( ( ... ) ) is a command substitution of a subshell {"echo $( (echo sub) ) > out", "sub\n"}, {"cat < out\n$((6 * 7))\nEOF", "42\n"}, {"case $((1 + 1)) in 2) echo two > out;; esac", "two\n"}, } for _, tc := range tests { if got := runIn(t, tc.input); got != tc.want { t.Errorf("%q: out = %q, want %q", tc.input, got, tc.want) } } } // goshOutput runs gosh -c script in a new process and returns its combined // output and exit status. func goshOutput(t *testing.T, script string, args ...string) (string, int) { t.Helper() cmd := exec.Command(subshellPath, append([]string{"-c", script, "gosh"}, args...)...) cmd.Dir = t.TempDir() out, err := cmd.CombinedOutput() return string(out), exitStatus(err) } func TestShell_Errexit(t *testing.T) { tests := []struct { script string want string status int }{ {"set -e; echo a; false; echo never", "a\n", 1}, {"set -e; sh -c 'exit 3'; echo never", "", 3}, {"set -e; gosh-no-such-cmd; echo never", "gosh: gosh-no-such-cmd: command not found\n", 127}, // Not for conditions, negated commands and && || lists (except the last) {"set -e; if false; then :; fi; while false; do :; done; until true; do :; done; echo ok", "ok\n", 0}, {"set -e; false || echo or; false && echo no; ! true; echo ok", "or\nok\n", 0}, {"set -e; true && false; echo never", "", 1}, {"set -e; false || false; echo never", "", 1}, {"set -e; f() { false; echo never; }; if f; then :; fi; echo cond-ok", "never\ncond-ok\n", 0}, // Also in functions, loops, subshells, pipelines and $(...) {"set -e; f() { false; echo never; }; f; echo never", "", 1}, {"set -e; for x in 1; do false; echo never; done", "", 1}, {"set -e; (false; echo never); echo never", "", 1}, {"set -e; (exit 4); echo never", "", 4}, {"set -e; true | false; echo never", "", 1}, {"set -e; x=$(false); echo never", "", 1}, {"set -e; set +e; false; echo off", "off\n", 0}, // gosh -e {"false; echo never", "", 1}, } for i, tc := range tests { script := tc.script var out string var status int if i == len(tests)-1 { cmd := exec.Command(subshellPath, "-e", "-c", script) b, err := cmd.CombinedOutput() out, status = string(b), exitStatus(err) } else { out, status = goshOutput(t, script) } if out != tc.want || status != tc.status { t.Errorf("%q: %q, status %d, want %q, %d", tc.script, out, status, tc.want, tc.status) } } } func TestShell_Options(t *testing.T) { tests := []struct { script string want string }{ {"set -x; x=1; echo \"a b\" $x", "+ x=1\n+ echo 'a b' 1\na b 1\n"}, {"set -x; f() { echo f; }; f", "+ f\n+ echo f\nf\n"}, // Each command once, also a function in a pipeline (in any order) {"f() { echo f; }; (set -x; f | cat) 2>&1 | sort", "+ cat\n+ echo f\n+ f\nf\n"}, {"set -x; set +x; echo quiet", "+ set +x\nquiet\n"}, {"false | true; echo $?; set -o pipefail; false | true; echo $?; sh -c 'exit 3' | false | true; echo $?", "0\n1\n1\n"}, {"set -o pipefail; true | true; echo $?", "0\n"}, {"set -euo pipefail; set -o", "errexit on\nnounset on\npipefail on\nxtrace off\n"}, {"set -e; set +o | grep errexit", "set -o errexit\n"}, {"set -- a b; set -e x y z; echo $# $1", "3 x\n"}, {"set -e; (set -o | grep errexit)", "errexit on\n"}, {"set +u", "gosh: set: +u: unset variables are always an error in gosh\n"}, {"set -k", "gosh: set: -k: invalid option\n"}, } for _, tc := range tests { if out, _ := goshOutput(t, tc.script); out != tc.want { t.Errorf("%q: %q, want %q", tc.script, out, tc.want) } } } func TestShell_EvalExec(t *testing.T) { tests := []struct { script string want string status int }{ {`cmd="echo a; echo b"; eval "$cmd"`, "a\nb\n", 0}, {`eval x=5; echo $x`, "5\n", 0}, {`x=y; y=deep; eval echo \$$x`, "deep\n", 0}, {`eval "f() { echo from-eval; }"; f`, "from-eval\n", 0}, {`eval "echo piped" | tr a-z A-Z`, "PIPED\n", 0}, {`eval 'exit 3'; echo never`, "", 3}, {`eval; echo empty`, "empty\n", 0}, {`eval "if"`, "gosh: eval: syntax error: unexpected end of input\n", 1}, {`for i in 1 2; do eval "v$i=$i"; done; echo $v1$v2`, "12\n", 0}, {`exec echo replaced; echo never`, "replaced\n", 0}, {`X=1 exec sh -c 'echo $X'`, "1\n", 0}, {`exec > out; echo to-file; exec >&2; cat out`, "to-file\n", 0}, {`exec 2> err; ls /gosh-no-such-dir; exec 2>&1; grep -c gosh-no-such-dir err`, "1\n", 0}, {`printf 'l1\nl2\n' > in; exec < in; read a; read b; echo $b $a`, "l2 l1\n", 0}, {`exec gosh-no-such-cmd; echo never`, "gosh: gosh-no-such-cmd: command not found\n", 127}, {`exec echo a | tr a A`, "A\n", 0}, } for _, tc := range tests { if out, status := goshOutput(t, tc.script); out != tc.want || status != tc.status { t.Errorf("%q: %q, status %d, want %q, %d", tc.script, out, status, tc.want, tc.status) } } } func TestShell_Trap(t *testing.T) { tests := []struct { script string want string status int }{ {`trap "echo bye" EXIT; echo running`, "running\nbye\n", 0}, {`trap 'echo "bye $?"' EXIT; exit 3`, "bye 3\n", 3}, {`trap 'echo "bye $?"' EXIT; false`, "bye 1\n", 1}, {`trap 'echo bye; exit 5' EXIT; true`, "bye\n", 5}, {`trap 'echo exit-trap' 0; f() { exit 2; }; f`, "exit-trap\n", 2}, {`trap 'echo set-e' EXIT; set -e; false; echo never`, "set-e\n", 1}, {`trap "echo bye" EXIT; trap - EXIT; echo no-trap`, "no-trap\n", 0}, {`trap 'echo got USR1' USR1; kill -USR1 $$; sleep 0.2; echo after`, "got USR1\nafter\n", 0}, // The trap runs after the current command (the signal arrives // asynchronously, hence the sleep) {`trap 'echo got $?' SIGUSR2; false; kill -USR2 $$; sleep 0.2; echo status $?`, "got 0\nstatus 0\n", 0}, {`trap '' USR1; kill -USR1 $$; echo ignored`, "ignored\n", 0}, {`trap '' USR1; sh -c 'kill -USR1 $$; echo child-ignores'`, "child-ignores\n", 0}, {`trap 'echo t' INT TERM; trap - INT; trap`, "trap -- 'echo t' TERM\n", 0}, {`trap 'echo t' INT; trap -p INT | cat`, "trap -- 'echo t' INT\n", 0}, {`trap 'echo cleanup' EXIT; kill -TERM $$; echo never`, "cleanup\n", 128 + 15}, {`trap 'echo term; exit 9' TERM; kill $$; echo never`, "term\n", 9}, {`trap 'echo x' BOGUS`, "gosh: trap: BOGUS: invalid signal specification\n", 1}, {`trap -l | grep -c TERM`, "1\n", 0}, // Traps are not inherited by subshells, ignored signals are {`trap 'echo parent' EXIT; (echo sub)`, "sub\nparent\n", 0}, } for _, tc := range tests { if out, status := goshOutput(t, tc.script); out != tc.want || status != tc.status { t.Errorf("%q: %q, status %d, want %q, %d", tc.script, out, status, tc.want, tc.status) } } } func TestShell_LastBackgroundPid(t *testing.T) { tests := []struct { script string want string }{ {`sleep 0.1 & pid=$!; case $pid in *[!0-9]*) echo bad;; *) echo pid;; esac; wait $pid; echo $?`, "pid\n0\n"}, {`sh -c 'exit 7' & wait $!; echo $?`, "7\n"}, {`echo ${!:-none}`, "none\n"}, {`sleep 0.1 & sleep 0.2 & first=$!; wait %1; echo $?; wait $first; echo $?`, "0\n0\n"}, } for _, tc := range tests { if out, _ := goshOutput(t, tc.script); out != tc.want { t.Errorf("%q: %q, want %q", tc.script, out, tc.want) } } }