add assignments name=value and unset
- name=value (one or more) without a command sets variables of the shell - name=value before a command sets them only in its environment (for builtins only while they run) - an assignment needs an unquoted name and =; the value is not split and not expanded as wildcard or brace pattern, ~ is expanded after = and : - the status of an assignment is the one of its last $(...) - new builtin unset Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fb94e98da5
commit
22ce8c00d4
5 files changed
+256
-14
No files matched your search
+109
@@ -0,0 +1,109 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user