add startup files and the source builtin
- login shells (-l or argv[0] starting with -) read /etc/gosh/profile and ~/.gosh_profile, interactive shells /etc/gosh/goshrc and ~/.goshrc; --noprofile and --norc skip them - source file / . file runs a file in the current shell, nesting is limited to stop files that source themselves - errors in startup files and scripts are reported with file and line - ~ and the startup files use $HOME (like sh), the user database is only the fallback Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1182d8d146
commit
4b44cc6e72
3 files changed
+304
-30
No files matched your search
+130
-5
@@ -889,6 +889,9 @@ func TestMain(m *testing.M) {
|
||||
panic(err)
|
||||
}
|
||||
os.Setenv("GOSH_HISTFILE", filepath.Join(dir, "history"))
|
||||
// Never read the system's startup files; tests that read the user's
|
||||
// files set $HOME themselves
|
||||
sysConfDir = filepath.Join(dir, "etc")
|
||||
code := m.Run()
|
||||
os.RemoveAll(dir)
|
||||
os.Exit(code)
|
||||
@@ -1183,7 +1186,7 @@ func TestShell_History(t *testing.T) {
|
||||
histFile := filepath.Join(t.TempDir(), "gosh", "history")
|
||||
workDir := t.TempDir()
|
||||
|
||||
cmd := exec.Command(bin, "-i")
|
||||
cmd := exec.Command(bin, "-i", "--norc")
|
||||
cmd.Dir = workDir
|
||||
cmd.Env = append(os.Environ(), "GOSH_HISTFILE="+histFile)
|
||||
cmd.Stdin = strings.NewReader("echo first\n set SECRET=hunter2\nexit\n")
|
||||
@@ -1215,7 +1218,7 @@ func TestShell_History(t *testing.T) {
|
||||
func TestShell_SIGINTKillsOnlyChild(t *testing.T) {
|
||||
bin := buildShell(t)
|
||||
|
||||
cmd := exec.Command(bin, "-i")
|
||||
cmd := exec.Command(bin, "-i", "--norc")
|
||||
// Own process group, so that we can signal it like the terminal does
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
stdin, err := cmd.StdinPipe()
|
||||
@@ -1313,7 +1316,7 @@ func TestRunScript(t *testing.T) {
|
||||
"touch first\n" +
|
||||
"false\n" +
|
||||
"touch second" // no trailing newline
|
||||
if got := runScript(strings.NewReader(script)); got != 0 {
|
||||
if got := runScript(strings.NewReader(script), ""); got != 0 {
|
||||
t.Errorf("runScript() = %d, want 0", got)
|
||||
}
|
||||
for _, name := range []string{"first", "second"} {
|
||||
@@ -1323,7 +1326,7 @@ func TestRunScript(t *testing.T) {
|
||||
}
|
||||
|
||||
// The status of the last command is returned
|
||||
if got := runScript(strings.NewReader("true\nsh -c 'exit 3'\n")); got != 3 {
|
||||
if got := runScript(strings.NewReader("true\nsh -c 'exit 3'\n"), ""); got != 3 {
|
||||
t.Errorf("runScript() = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
@@ -1335,7 +1338,7 @@ func TestRun_Options(t *testing.T) {
|
||||
}{
|
||||
{[]string{"-c", "true"}, 0},
|
||||
{[]string{"-c", "false"}, 1},
|
||||
{[]string{"-l", "-c", "sh -c 'exit 5'"}, 5},
|
||||
{[]string{"-l", "--noprofile", "-c", "sh -c 'exit 5'"}, 5},
|
||||
{[]string{"-c"}, 2},
|
||||
{[]string{"-x"}, 2},
|
||||
{[]string{"/gosh/no/such/script"}, 127},
|
||||
@@ -1730,3 +1733,125 @@ func TestExecInput_Wildcards(t *testing.T) {
|
||||
t.Errorf("expected ambiguous redirect error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartupFiles(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
sys := sysConfDir
|
||||
|
||||
tests := []struct {
|
||||
login, interactive bool
|
||||
want []string
|
||||
}{
|
||||
{false, false, nil},
|
||||
{true, false, []string{sys + "/profile", home + "/.gosh_profile"}},
|
||||
{false, true, []string{sys + "/goshrc", home + "/.goshrc"}},
|
||||
{true, true, []string{sys + "/profile", home + "/.gosh_profile", sys + "/goshrc", home + "/.goshrc"}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := startupFiles(tc.login, tc.interactive); !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("startupFiles(%v, %v) = %q, want %q", tc.login, tc.interactive, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_LoginReadsProfile(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
os.Unsetenv("GOSH_TEST_PROFILE")
|
||||
defer os.Unsetenv("GOSH_TEST_PROFILE")
|
||||
os.WriteFile(filepath.Join(home, ".gosh_profile"), []byte("export GOSH_TEST_PROFILE=yes\n"), 0644)
|
||||
// .goshrc is only read by interactive shells
|
||||
os.WriteFile(filepath.Join(home, ".goshrc"), []byte("exit 99\n"), 0644)
|
||||
|
||||
check := `sh -c 'test "$GOSH_TEST_PROFILE" = yes'`
|
||||
if got := run([]string{"-c", check}); got != 1 {
|
||||
t.Errorf("non-login shell: status %d, want 1 (profile must not be read)", got)
|
||||
}
|
||||
if got := run([]string{"-l", "--noprofile", "-c", check}); got != 1 {
|
||||
t.Errorf("--noprofile: status %d, want 1 (profile must not be read)", got)
|
||||
}
|
||||
if got := run([]string{"-l", "-c", check}); got != 0 {
|
||||
t.Errorf("login shell: status %d, want 0 (profile must be read)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShell_InteractiveReadsRC(t *testing.T) {
|
||||
bin := buildShell(t)
|
||||
home := t.TempDir()
|
||||
os.WriteFile(filepath.Join(home, ".goshrc"), []byte("export GOSH_TEST_RC=from-rc\n"), 0644)
|
||||
os.WriteFile(filepath.Join(home, ".gosh_profile"), []byte("export GOSH_TEST_PROFILE=from-profile\n"), 0644)
|
||||
|
||||
tests := []struct {
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{[]string{"-i"}, "from-rc -\n"},
|
||||
{[]string{"-i", "--norc"}, "- -\n"},
|
||||
{[]string{"-i", "-l"}, "from-rc from-profile\n"},
|
||||
{[]string{"-i", "-l", "--norc", "--noprofile"}, "- -\n"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
cmd := exec.Command(bin, tc.args...)
|
||||
cmd.Env = append(os.Environ(), "HOME="+home)
|
||||
cmd.Stdin = strings.NewReader("echo ${GOSH_TEST_RC:--} ${GOSH_TEST_PROFILE:--}\nexit\n")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatalf("%v: %v", tc.args, err)
|
||||
}
|
||||
if !strings.HasSuffix(string(out), tc.want) {
|
||||
t.Errorf("%v: output %q, want suffix %q", tc.args, out, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecInput_Source(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
os.Unsetenv("GOSH_TEST_SOURCE")
|
||||
defer os.Unsetenv("GOSH_TEST_SOURCE")
|
||||
|
||||
file := filepath.Join(dir, "vars")
|
||||
os.WriteFile(file, []byte("# comment\nexport GOSH_TEST_SOURCE=hello\nsh -c 'exit 4'\n"), 0644)
|
||||
|
||||
for _, cmd := range []string{"source", "."} {
|
||||
os.Unsetenv("GOSH_TEST_SOURCE")
|
||||
execInput(cmd + " " + file)
|
||||
if os.Getenv("GOSH_TEST_SOURCE") != "hello" {
|
||||
t.Errorf("%s: variable was not set in the current shell", cmd)
|
||||
}
|
||||
if lastStatus != 4 {
|
||||
t.Errorf("%s: $? = %d, want 4 (status of the last command)", cmd, lastStatus)
|
||||
}
|
||||
}
|
||||
|
||||
err := execInput("source " + filepath.Join(dir, "missing"))
|
||||
if err == nil || !strings.Contains(err.Error(), "source: "+dir+"/missing: no such file or directory") {
|
||||
t.Errorf("unexpected error for missing file: %v", err)
|
||||
}
|
||||
if lastStatus != 1 {
|
||||
t.Errorf("$? = %d for missing file, want 1", lastStatus)
|
||||
}
|
||||
|
||||
// A file that sources itself is stopped
|
||||
loop := filepath.Join(dir, "loop")
|
||||
os.WriteFile(loop, []byte("source "+loop+"\n"), 0644)
|
||||
if err := execInput("source " + loop); err == nil {
|
||||
t.Error("expected an error for a file that sources itself")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHomeDir_UsesHOME(t *testing.T) {
|
||||
t.Setenv("HOME", "/gosh/test/home")
|
||||
if got := getHomeDir(); got != "/gosh/test/home" {
|
||||
t.Errorf("getHomeDir() = %q, want $HOME", got)
|
||||
}
|
||||
if words, _ := splitWords("echo ~/x"); words[1] != "/gosh/test/home/x" {
|
||||
t.Errorf("~/x = %q, want it based on $HOME", words[1])
|
||||
}
|
||||
|
||||
// Without $HOME the user database is used
|
||||
t.Setenv("HOME", "")
|
||||
if got := getHomeDir(); got == "" {
|
||||
t.Error("getHomeDir() is empty without $HOME")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user