feat(sunrise): print the PATH hint for the user's login shell
- new.sh reads the basename of SHELL (via ${SHELL:-} under set -u) and
prints fish_add_path "<dir>" for fish, which keeps it for new sessions,
export PATH="<dir>:$PATH" naming ~/.zshrc or ~/.bashrc for zsh and
bash, and today's generic export line with "your shell profile" for
any other, empty or unset shell
- dq_escape escapes the install directory for the target shell's double
quotes: backslash, double quote and dollar always, plus backtick and a
single-quoted ! for POSIX shells; a per-character loop, so bash 3.2
and 5.2 behave the same
- the behaviour harness defaults SHELL to empty so cases do not depend on
the developer's login shell, adds an unset-SHELL mode and covers fish,
zsh, bash, empty, unset, unknown and two metacharacter directories
- TestBootstrapEndToEnd pins SHELL=/bin/bash
- sunrise README and docs/setup/installation.md describe the per-shell
hint; the manual PATH section gains fish_add_path (go env GOPATH)/bin
This commit is contained in:
@@ -30,6 +30,7 @@ type scriptHarness struct {
|
||||
goVersion string // go env GOVERSION
|
||||
useGOBIN bool // go env GOBIN prints gobin; otherwise empty, GOPATH/bin is used
|
||||
gobinOnPath bool
|
||||
unsetShell bool // run the script with SHELL unset instead of the empty default
|
||||
env []string // extra environment
|
||||
}
|
||||
|
||||
@@ -243,6 +244,10 @@ func (h *scriptHarness) run(args ...string) scriptResult {
|
||||
"STUB_GOVERSION="+h.goVersion,
|
||||
"STUB_GOBIN="+gobin,
|
||||
"STUB_GOPATH="+h.gopath,
|
||||
// bash fills an unset SHELL from the passwd entry, so this default
|
||||
// keeps cases independent of the developer's login shell; a case
|
||||
// sets its own SHELL through h.env, which comes later and wins.
|
||||
"SHELL=",
|
||||
)
|
||||
// The script must not depend on the harness identity: drop it.
|
||||
filtered := env[:0]
|
||||
@@ -254,6 +259,11 @@ func (h *scriptHarness) run(args ...string) scriptResult {
|
||||
}
|
||||
env = append(filtered, h.env...)
|
||||
cmd := exec.Command(h.bash, append([]string{h.script}, args...)...)
|
||||
if h.unsetShell {
|
||||
// Unset SHELL inside bash, after startup filled it, then source the
|
||||
// script ($0) with the case arguments so it runs with SHELL unset.
|
||||
cmd = exec.Command(h.bash, append([]string{"-c", `unset SHELL; . "$0" "$@"`, h.script}, args...)...)
|
||||
}
|
||||
cmd.Dir = h.work
|
||||
cmd.Env = env
|
||||
cmd.Stdin = strings.NewReader(stdinProbe)
|
||||
@@ -319,6 +329,57 @@ func expectContains(t *testing.T, what, got string, wants ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func expectLacks(t *testing.T, what, got string, unwanted ...string) {
|
||||
t.Helper()
|
||||
for _, u := range unwanted {
|
||||
if strings.Contains(got, u) {
|
||||
t.Fatalf("%s contains %q:\n%s", what, u, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hintDir returns dir escaped the way new.sh escapes it inside the double
|
||||
// quotes of the printed PATH hint: backslash, double quote and dollar for
|
||||
// every shell, plus backtick and a single-quoted ! for POSIX shells.
|
||||
func hintDir(dir string, fish bool) string {
|
||||
if fish {
|
||||
return strings.NewReplacer(`\`, `\\`, `"`, `\"`, `$`, `\$`).Replace(dir)
|
||||
}
|
||||
return strings.NewReplacer(`\`, `\\`, `"`, `\"`, `$`, `\$`, "`", "\\`", `!`, `"'!'"`).Replace(dir)
|
||||
}
|
||||
|
||||
// hint is the PATH hint new.sh prints at the end of stdout.
|
||||
func hint(gobin, note, line string) string {
|
||||
return "\n==> summer is installed in " + gobin + ", which is not on your PATH. Run this (" + note + "):\n" + line + "\n"
|
||||
}
|
||||
|
||||
// exportHint and fishHint are the two hint forms for gobin.
|
||||
func exportHint(gobin, note string) string {
|
||||
return hint(gobin, note, ` export PATH="`+hintDir(gobin, false)+`:$PATH"`)
|
||||
}
|
||||
|
||||
func fishHint(gobin string) string {
|
||||
return hint(gobin, "fish_add_path also keeps it for new fish sessions", ` fish_add_path "`+hintDir(gobin, true)+`"`)
|
||||
}
|
||||
|
||||
// expectHintSuffix asserts stdout ends with the hint.
|
||||
func expectHintSuffix(t *testing.T, stdout, want string) {
|
||||
t.Helper()
|
||||
if !strings.HasSuffix(stdout, want) {
|
||||
t.Fatalf("stdout does not end with the hint\ngot:\n%q\nwant suffix:\n%q", stdout, want)
|
||||
}
|
||||
}
|
||||
|
||||
// metaGobin creates and selects a Go bin directory whose name holds a
|
||||
// space, backslash, double quote, dollar, backtick and !.
|
||||
func metaGobin(t *testing.T, h *scriptHarness) {
|
||||
t.Helper()
|
||||
h.gobin = filepath.Join(h.root, "go bin\\x\"y$HOME`id`!z")
|
||||
if err := os.MkdirAll(h.gobin, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// expectNothingStarted asserts no clone and no go or summer call happened.
|
||||
func expectNothingStarted(t *testing.T, h *scriptHarness) {
|
||||
t.Helper()
|
||||
@@ -583,16 +644,61 @@ func TestScriptBehavior(t *testing.T) {
|
||||
t.Fatalf("the script created .git: %v", err)
|
||||
}
|
||||
}},
|
||||
{"GOBIN off PATH prints the hint", func(t *testing.T, h *scriptHarness) {
|
||||
{"empty SHELL prints the generic hint", func(t *testing.T, h *scriptHarness) {
|
||||
res := h.mustRun("demo")
|
||||
expectContains(t, "stdout", res.stdout, "not on your PATH", `export PATH="`+h.gobin+`:$PATH"`)
|
||||
expectHintSuffix(t, res.stdout, "\n==> summer is installed in "+h.gobin+", which is not on your PATH. Run this (and add it to your shell profile):\n export PATH=\""+h.gobin+":$PATH\"\n")
|
||||
}},
|
||||
{"unset SHELL prints the generic hint", func(t *testing.T, h *scriptHarness) {
|
||||
h.unsetShell = true
|
||||
res := h.mustRun("demo")
|
||||
expectLacks(t, "stderr", res.stderr, "unbound variable")
|
||||
expectHintSuffix(t, res.stdout, exportHint(h.gobin, "and add it to your shell profile"))
|
||||
}},
|
||||
{"unknown SHELL prints the generic hint", func(t *testing.T, h *scriptHarness) {
|
||||
h.env = append(h.env, "SHELL=/bin/tcsh")
|
||||
res := h.mustRun("demo")
|
||||
expectHintSuffix(t, res.stdout, exportHint(h.gobin, "and add it to your shell profile"))
|
||||
}},
|
||||
{"fish SHELL prints fish_add_path", func(t *testing.T, h *scriptHarness) {
|
||||
h.env = append(h.env, "SHELL=/usr/bin/fish")
|
||||
res := h.mustRun("demo")
|
||||
expectHintSuffix(t, res.stdout, fishHint(h.gobin))
|
||||
expectContains(t, "stdout", res.stdout, "fish_add_path also keeps it for new fish sessions", ` fish_add_path "`+h.gobin+`"`)
|
||||
expectLacks(t, "stdout", res.stdout, "export PATH", "shell profile")
|
||||
}},
|
||||
{"zsh SHELL names ~/.zshrc", func(t *testing.T, h *scriptHarness) {
|
||||
h.env = append(h.env, "SHELL=/bin/zsh")
|
||||
res := h.mustRun("demo")
|
||||
expectHintSuffix(t, res.stdout, exportHint(h.gobin, "and add it to ~/.zshrc"))
|
||||
expectLacks(t, "stdout", res.stdout, "fish_add_path")
|
||||
}},
|
||||
{"bash SHELL names ~/.bashrc", func(t *testing.T, h *scriptHarness) {
|
||||
h.env = append(h.env, "SHELL=/usr/local/bin/bash")
|
||||
res := h.mustRun("demo")
|
||||
expectHintSuffix(t, res.stdout, exportHint(h.gobin, "and add it to ~/.bashrc"))
|
||||
expectLacks(t, "stdout", res.stdout, "fish_add_path")
|
||||
}},
|
||||
{"metacharacters in the dir are escaped for zsh", func(t *testing.T, h *scriptHarness) {
|
||||
metaGobin(t, h)
|
||||
h.env = append(h.env, "SHELL=/bin/zsh")
|
||||
res := h.mustRun("demo")
|
||||
want := " export PATH=\"" + hintDir(h.root, false) + "/" + (`go bin\\x\"y\$HOME\` + "`" + `id\` + "`" + `"'!'"z`) + ":$PATH\"\n"
|
||||
expectContains(t, "stdout", res.stdout, want)
|
||||
expectHintSuffix(t, res.stdout, exportHint(h.gobin, "and add it to ~/.zshrc"))
|
||||
}},
|
||||
{"metacharacters in the dir are escaped for fish", func(t *testing.T, h *scriptHarness) {
|
||||
metaGobin(t, h)
|
||||
h.env = append(h.env, "SHELL=/usr/bin/fish")
|
||||
res := h.mustRun("demo")
|
||||
want := " fish_add_path \"" + hintDir(h.root, true) + "/" + (`go bin\\x\"y\$HOME` + "`id`!z") + "\"\n"
|
||||
expectContains(t, "stdout", res.stdout, want)
|
||||
expectHintSuffix(t, res.stdout, fishHint(h.gobin))
|
||||
}},
|
||||
{"GOBIN on PATH prints no hint", func(t *testing.T, h *scriptHarness) {
|
||||
h.gobinOnPath = true
|
||||
h.env = append(h.env, "SHELL=/usr/bin/fish")
|
||||
res := h.mustRun("demo")
|
||||
if strings.Contains(res.stdout, "export PATH") || strings.Contains(res.stdout, "not on your PATH") {
|
||||
t.Fatalf("hint printed although GOBIN is on PATH:\n%s", res.stdout)
|
||||
}
|
||||
expectLacks(t, "stdout", res.stdout, "export PATH", "fish_add_path", "not on your PATH")
|
||||
}},
|
||||
{"empty GOBIN installs into GOPATH/bin", func(t *testing.T, h *scriptHarness) {
|
||||
h.useGOBIN = false
|
||||
@@ -605,7 +711,7 @@ func TestScriptBehavior(t *testing.T) {
|
||||
expectContains(t, "stdout", res.stdout, `export PATH="`+bin+`:$PATH"`)
|
||||
}},
|
||||
}
|
||||
if len(cases) < 18 {
|
||||
if len(cases) < 32 {
|
||||
t.Fatalf("only %d cases", len(cases))
|
||||
}
|
||||
for _, tc := range cases {
|
||||
|
||||
Reference in New Issue
Block a user