diff --git a/root/init.go b/root/init.go index b33776f1..a9ff6463 100644 --- a/root/init.go +++ b/root/init.go @@ -33,7 +33,7 @@ fi # a non-interactive shell (tool runners sourcing rc files, scripts) has no # prompt to complete, and exec'ing here would seize the tty from the real iris -if [[ -o interactive ]] && [ -t 0 ] && [ -z "$IRIS_PID" ] && [ -z "$IRIS_RESCUE" ]; then +if [[ -o interactive ]] && [ -t 0 ] && [ -z "$IRIS_PID" ] && [ -z "$IRIS_RESCUE" ] && [ -z "$ZSH_EXECUTION_STRING" ]; then export IRIS_ACTIVE_SHELL="zsh" if [ -n "$FNM_MULTISHELL_PATH" ]; then path=(${path:#*fnm_multishells*}) @@ -84,7 +84,7 @@ fi # a non-interactive shell (tool runners sourcing rc files, scripts) has no # prompt to complete, and exec'ing here would seize the tty from the real iris -if [[ $- == *i* ]] && [ -t 0 ] && [ -z "$IRIS_PID" ] && [ -z "$IRIS_RESCUE" ]; then +if [[ $- == *i* ]] && [ -t 0 ] && [ -z "$IRIS_PID" ] && [ -z "$IRIS_RESCUE" ] && [ -z "$BASH_EXECUTION_STRING" ]; then export IRIS_ACTIVE_SHELL="bash" if [ -n "$FNM_MULTISHELL_PATH" ]; then PATH=":$PATH:" @@ -134,7 +134,7 @@ if set -q IRIS_PID end end -if status is-interactive; and not set -q IRIS_PID; and not set -q IRIS_RESCUE +if status is-interactive; and test -t 0; and not set -q IRIS_PID; and not set -q IRIS_RESCUE set -gx IRIS_ACTIVE_SHELL "fish" if set -q FNM_MULTISHELL_PATH set -gx PATH (string match -v "*fnm_multishells*" $PATH) diff --git a/root/init_test.go b/root/init_test.go index 32f00ac0..eefea14a 100644 --- a/root/init_test.go +++ b/root/init_test.go @@ -83,3 +83,32 @@ func TestFishAutosuggestionsFollowGhostTextMode(t *testing.T) { } } } + +func TestInitAutostartSkipsExecutionString(t *testing.T) { + guards := map[string]string{ + "zsh": `[ -z "$ZSH_EXECUTION_STRING" ]`, + "bash": `[ -z "$BASH_EXECUTION_STRING" ]`, + "fish": "test -t 0", + } + + for shell, guard := range guards { + t.Run(shell, func(t *testing.T) { + script := captureInitScript(t, shell) + + head, _, found := strings.Cut(script, "exec iris") + if !found { + t.Fatalf("%s init script has no autostart", shell) + } + + cond := strings.LastIndex(head, "\nif ") + if cond < 0 { + t.Fatalf("%s autostart is not inside an if", shell) + } + + if !strings.Contains(head[cond:], guard) { + t.Fatalf("%s autostart is not guarded by %q:\n%s", shell, guard, head[cond:]) + } + }) + } +} + diff --git a/tests/tui/autostart_test.go b/tests/tui/autostart_test.go new file mode 100644 index 00000000..a638fb98 --- /dev/null +++ b/tests/tui/autostart_test.go @@ -0,0 +1,90 @@ +package tui + +import ( + "bytes" + "context" + "io" + "os/exec" + "testing" + "time" + + "github.com/creack/pty" +) + +func TestAutostartDoesNotSwallowCommand(t *testing.T) { + bin := binary(t) + + t.Run("bash", func(t *testing.T) { + if _, err := exec.LookPath("bash"); err != nil { + t.Skip("bash not installed") + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + initScript, err := exec.CommandContext(ctx, bin, "init", "bash").Output() + if err != nil { + t.Fatalf("iris init bash: %v", err) + } + cmd := exec.CommandContext(ctx, "bash", "--norc", "-i", "-c", string(initScript)+"\necho RAN_BASH_42") + ptmx, err := pty.Start(cmd) + if err != nil { + t.Fatalf("starting bash in pty: %v", err) + } + defer func() { _ = ptmx.Close() }() + + var out bytes.Buffer + done := make(chan error, 1) + go func() { + _, errCopy := io.Copy(&out, ptmx) + done <- errCopy + }() + + select { + case <-ctx.Done(): + _ = cmd.Process.Kill() + t.Fatalf("timed out waiting for bash command, output:\n%s", out.String()) + case <-done: + } + + if !bytes.Contains(out.Bytes(), []byte("RAN_BASH_42")) { + t.Fatalf("expected output to contain RAN_BASH_42, got:\n%s", out.String()) + } + }) + + t.Run("zsh", func(t *testing.T) { + if _, err := exec.LookPath("zsh"); err != nil { + t.Skip("zsh not installed") + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + initScript, err := exec.CommandContext(ctx, bin, "init", "zsh").Output() + if err != nil { + t.Fatalf("iris init zsh: %v", err) + } + cmd := exec.CommandContext(ctx, "zsh", "-f", "-i", "-c", string(initScript)+"\necho RAN_ZSH_42") + ptmx, err := pty.Start(cmd) + if err != nil { + t.Fatalf("starting zsh in pty: %v", err) + } + defer func() { _ = ptmx.Close() }() + + var out bytes.Buffer + done := make(chan error, 1) + go func() { + _, errCopy := io.Copy(&out, ptmx) + done <- errCopy + }() + + select { + case <-ctx.Done(): + _ = cmd.Process.Kill() + t.Fatalf("timed out waiting for zsh command, output:\n%s", out.String()) + case <-done: + } + + if !bytes.Contains(out.Bytes(), []byte("RAN_ZSH_42")) { + t.Fatalf("expected output to contain RAN_ZSH_42, got:\n%s", out.String()) + } + }) +}