package main import ( "os" "testing" ) func TestTokenize_Assignments(t *testing.T) { t.Setenv("HOME", "/home/u") tests := []struct { input string word string assign bool }{ {"x=1", "x=1", true}, {"_a1=", "_a1=", true}, {`x="a b"`, "x=a b", true}, {"x=a=b", "x=a=b", true}, {"P=~/bin:~/go/bin", "P=/home/u/bin:/home/u/go/bin", true}, {"x=a~b", "x=a~b", true}, {"x=~", "x=/home/u", true}, // Not an assignment: quoted or escaped name or =, invalid name {`"x=1"`, "x=1", false}, {`x"=1"`, "x=1", false}, {`x\=1`, "x=1", false}, {"1x=2", "1x=2", false}, {"=x", "=x", false}, {"a-b=1", "a-b=1", false}, } for _, tc := range tests { tokens, err := tokenize(tc.input) if err != nil || len(tokens) != 1 { t.Errorf("tokenize(%q) = %+v, %v", tc.input, tokens, err) continue } if tokens[0].word != tc.word || tokens[0].assign != tc.assign { t.Errorf("tokenize(%q) = %q (assign %v), want %q (assign %v)", tc.input, tokens[0].word, tokens[0].assign, tc.word, tc.assign) } } // The output of $(...) is not split in an assignment tokens, _ := tokenize("x=$(echo a b) y") if len(tokens) != 2 || tokens[0].word != "x=a b" || !tokens[0].assign { t.Errorf("x=$(echo a b): %+v", tokens) } } func TestExec_Assignments(t *testing.T) { for _, name := range []string{"GOSH_A", "GOSH_B", "GOSH_C"} { os.Unsetenv(name) defer os.Unsetenv(name) } tests := []struct { input string want string }{ {"GOSH_A=1; echo $GOSH_A > out", "1\n"}, {"GOSH_A=1 GOSH_B=2; echo $GOSH_A$GOSH_B > out", "12\n"}, {`GOSH_A="a b"; echo "$GOSH_A" > out`, "a b\n"}, {"GOSH_A=$(printf 'a b\\nc'); echo \"$GOSH_A\" > out", "a b\nc\n"}, // No wildcard or brace expansion in the value {"touch x.go; GOSH_A=*.go; echo \"$GOSH_A\" > out", "*.go\n"}, {"GOSH_A={a,b}; echo $GOSH_A > out", "{a,b}\n"}, // ... but as an argument, assignment-like words are normal words {"echo GOSH_A={a,b} > out", "GOSH_A=a GOSH_A=b\n"}, // Before a command, only in its environment {"GOSH_C=temp sh -c 'echo $GOSH_C' > out; echo ${GOSH_C:-unset} >> out", "temp\nunset\n"}, {"GOSH_A=old; GOSH_A=new sh -c 'echo $GOSH_A' > out; echo $GOSH_A >> out", "new\nold\n"}, // ... and before a builtin only while it runs {"GOSH_C=temp pwd > /dev/null; echo ${GOSH_C:-unset} > out", "unset\n"}, // In a pipeline or with a command they do not change the shell {"GOSH_C=x | cat; echo ${GOSH_C:-unset} > out", "unset\n"}, // Redirections are applied {"GOSH_A=r > out", ""}, // In loops {"for i in 1 2 3; do GOSH_A=$i; done; echo $GOSH_A > out", "3\n"}, {"GOSH_B=0; while [ $GOSH_B -lt 3 ]; do echo $GOSH_B >> out; GOSH_B=$(expr $GOSH_B + 1); done", "0\n1\n2\n"}, // Not assignments {`echo "GOSH_A=1" GOSH_B=2 > out`, "GOSH_A=1 GOSH_B=2\n"}, // unset {"GOSH_A=1; unset GOSH_A; echo ${GOSH_A:-gone} > out", "gone\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) } } // The status of an assignment is the one of its last $(...) for input, want := range map[string]int{ "GOSH_A=1": 0, "GOSH_A=$(false)": 1, "GOSH_A=$(sh -c 'exit 3')": 3, "GOSH_A=$(false) GOSH_B=$(true)": 0, } { runIn(t, input) if lastStatus != want { t.Errorf("%q: $? = %d, want %d", input, lastStatus, want) } } if err := execInput("unset 1x"); err == nil || err.Error() != "unset: `1x': not a valid identifier" { t.Errorf("unset 1x: %v", err) } if err := execInput(`"GOSH_A=1"`); err == nil || exitStatus(err) != 127 { t.Errorf(`"GOSH_A=1" must be a command: %v`, err) } }