name: "Interactive" incompatible_configs: ["sh"] cases: - name: "Basic interactive test" pty: true ignore_stdout: true # Needs investigation ignore_whitespace: true # There's an extra newline for now in test output stdin: | #expect-prompt echo hi #send:Enter #expect:hi #expect-prompt #send:Ctrl+D - name: "SIGINT ends the command an interactive shell is running" known_failure: true # TODO(signals): an interactive brush goes on with the command it's running pty: true ignore_stdout: true # Prompts don't line up stdin: | #expect-prompt kill -INT $$; touch went-on.txt #send:Enter #expect-prompt #send:Ctrl+D - name: "Programs inherit an ignored SIGINT after the shell first waits for one" skip: true # TODO(signals): flaky; tokio's SIGINT listener replaces the inherited ignore, but a wait for a program sets it up only once its select! polls it (in random order), so the ignore survives when the first program has already exited pty: true ignore_stdout: true # Prompts don't line up # The shell replaces itself with one started ignoring SIGINT, given the flags the harness # gives brush; it waits for one program, and then another sends itself SIGINT. stdin: | #expect-prompt exec sh -c 'p=$1; shift; trap "" INT; exec env PS1="$p" "$0" --norc --noprofile -i "$@"' "$BASH" "$PS1" ${BRUSH_VERSION:+--no-config --disable-bracketed-paste --disable-color --input-backend=basic} #send:Enter #expect-prompt sh -c 'exit 0'; sh -c 'kill -INT $$; touch survived.txt'; echo "done-$((2 + 2))" #send:Enter #expect:done-4 #expect-prompt #send:Ctrl+D - name: "An interactive shell's inherited _ isn't exported by its first prompt" pty: true ignore_stdout: true # Prompts don't line up min_oracle_version: "5.3" # bash 5.2 keeps the inherited `_` exported until a command updates it # The shell replaces itself with an interactive one that inherits `_`, given the flags the # harness gives brush; that one's first command passes on whatever `_` it exports then. stdin: | #expect-prompt exec env _=/inherited/underscore PS1="$PS1" "$BASH" --norc --noprofile -i ${BRUSH_VERSION:+--no-config --disable-bracketed-paste --disable-color --input-backend=basic} #send:Enter #expect-prompt exec printenv _ > out.txt #send:Enter - name: "Simple tab completion" skip: true # TODO(input): Need to find the right way to enable this with a rich input backend. pty: true ignore_stdout: true test_files: - path: "test-file.txt" contents: | Hello, world. stdin: | #expect-prompt cat test-file. #send:Tab #send:Enter #expect:Hello, world. #expect-prompt #send:Ctrl+D - name: "PROMPT_COMMAND" ignore_stderr: true args: ["-i"] stdin: | declare -a calls=() command=0 PROMPT_COMMAND='calls+=("$command")' command=first command=second command=third unset PROMPT_COMMAND declare -p calls # Reading commands from a pipe doesn't make a shell interactive, so no prompt is # displayed and PROMPT_COMMAND never runs. - name: "PROMPT_COMMAND in a non-interactive shell reading stdin" args: ["-s"] stdin: | PROMPT_COMMAND='echo PROMPT-COMMAND-RAN' echo one echo two # Nor is the prompt itself expanded there. PS1 is left single-quoted so the command # substitution runs at prompt time rather than at assignment time, and it writes to stderr # to escape its own capture -- expanding this prompt is otherwise silent. - name: "prompt expansion in a non-interactive shell reading stdin" args: ["-s"] stdin: | PS1='$(echo PS1-EXPANDED >&2)$ ' echo one echo two # PS0 is expanded after a command is read and before it runs, and only by an interactive # shell -- so its command substitution has no more business running here than PS1's does. - name: "PS0 expansion in a non-interactive shell reading stdin" args: ["-s"] stdin: | PS0='$(echo PS0-EXPANDED >&2)' echo one echo two # PROMPT_COMMAND runs between commands, so what it leaves in $?, PIPESTATUS, and $_ must # not be visible to the next command. - name: "PROMPT_COMMAND preserves the last argument in $_" ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell args: ["-i"] stdin: | PROMPT_COMMAND='true PC-ARG' true USER-ARG echo "UNDERSCORE[$_]" unset PROMPT_COMMAND # bash accepts a string or an indexed array in PROMPT_COMMAND and ignores any other type. - name: "PROMPT_COMMAND as an indexed array" ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell args: ["-i"] stdin: | PROMPT_COMMAND=('echo INDEXED-0' 'echo INDEXED-1') echo command-ran unset PROMPT_COMMAND # Each element is saved/restored on its own, so element N sees the status of the last # *user* command rather than whatever element N-1 left behind. `(exit 6)` is what makes # the two distinguishable: a leak from `false` would print 1 here, not 6. - name: "PROMPT_COMMAND array elements each see the last command's status" ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell args: ["-i"] stdin: | PROMPT_COMMAND=('false' 'echo PC-SAW[$?]') (exit 6) echo "AFTER[$?]" unset PROMPT_COMMAND # An `exit` from PROMPT_COMMAND ends the shell then and there: the remaining array # elements don't run, the command waiting on stdin never does, and the shell exits with # the status `exit` was given. - name: "exit from PROMPT_COMMAND ends the shell with its status" ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell args: ["-i"] stdin: | PROMPT_COMMAND=('echo PC-A' 'exit 7' 'echo PC-C') echo NEVER-RUNS - name: "PROMPT_COMMAND as an associative array is ignored" ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell args: ["-i"] stdin: | declare -A PROMPT_COMMAND PROMPT_COMMAND[x]='echo ASSOC-X' PROMPT_COMMAND[y]='echo ASSOC-Y' echo command-ran unset PROMPT_COMMAND # `return` outside a function or sourced script requests no control flow at all -- the # builtin only does so when `in_function()` or `in_sourced_script()` -- so neither a # PROMPT_COMMAND entry nor a line typed at the prompt can end the shell this way. bash # reports the error and carries on, and the remaining array elements still run. - name: "return does not exit the shell, from PROMPT_COMMAND or the prompt" ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell args: ["-i"] stdin: | PROMPT_COMMAND=('echo PC-A' 'return' 'echo PC-B') echo STILL-ALIVE unset PROMPT_COMMAND return echo STILL-ALIVE-AFTER-BARE-RETURN # Expanding the prompt runs commands too (command substitutions in PS1), so what it leaves # behind must not be visible to the next command either. - name: "prompt expansion preserves the last argument in $_" ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell args: ["-i"] stdin: | PS1='$(true PS1-ARG)$ ' true USER-ARG echo "UNDERSCORE[$_]" - name: "prompt expansion preserves $? and PIPESTATUS" ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell args: ["-i"] stdin: | PS1='$( (exit 9) | (exit 8) )$ ' (exit 4) | (exit 5) echo "AFTER[$?][${PIPESTATUS[*]}]" - name: "Interactive -c running a program" known_failure: true # TODO(jobs): the program takes the terminal's foreground from a background process group, and is stopped by SIGTTOU for good pty: true timeout_in_seconds: 5 args: ["-i", "-c", "cat /dev/null; echo done"] stdin: | #expect:done - name: "A fatal error in a function drops the rest of the interactive line" known_failure: true # TODO(interactive): runs the rest of the line pty: true ignore_stdout: true # Prompts don't line up stdin: | #expect-prompt f() { set -u; echo "$unset_var"; }; f; touch went-on.txt #send:Enter #expect-prompt #send:Ctrl+D - name: "A fatal error in a builtin drops the rest of the interactive line" known_failure: true # TODO(interactive): runs the rest of the line pty: true ignore_stdout: true # Prompts don't line up stdin: | #expect-prompt set -u; compgen -W '$unset_var'; touch went-on.txt #send:Enter #expect-prompt #send:Ctrl+D - name: "A fatal error in a builtin ends an interactive shell's subshell" known_failure: true # TODO(interactive): the subshell carries on pty: true ignore_stdout: true # Prompts don't line up stdin: | #expect-prompt ( set -u; compgen -W '$unset_var'; touch went-on.txt ) #send:Enter #expect-prompt #send:Ctrl+D - name: "A fatal expansion error drops the rest of the interactive line" pty: true ignore_stdout: true # Prompts don't line up stdin: | #expect-prompt set -u; echo "$unset_var"; touch went-on.txt #send:Enter #expect-prompt #send:Ctrl+D - name: "A fatal expansion error ends an interactive shell's subshell" pty: true ignore_stdout: true # Prompts don't line up stdin: | #expect-prompt ( set -u; echo "$unset_var"; touch went-on.txt ) #send:Enter #expect-prompt #send:Ctrl+D - name: "A fatal error in eval drops only the eval'd code in an interactive shell" pty: true ignore_stdout: true # Prompts don't line up stdin: | #expect-prompt set -u; eval 'echo "$unset_var"; touch skipped.txt'; touch went-on.txt #send:Enter #expect-prompt #send:Ctrl+D - name: "A fatal error in a sourced file drops only that file in an interactive shell" pty: true ignore_stdout: true # Prompts don't line up test_files: - path: "script.sh" contents: | echo "$unset_var" touch skipped.txt stdin: | #expect-prompt set -u; . ./script.sh; touch went-on.txt #send:Enter #expect-prompt #send:Ctrl+D