Issue
I'm writing a dummy shell in nodejs to figure out how they work, and have realised a hole in my understanding. Specifically, in NodeJS, you can detect a shell in TTY mode using process.<stream>.isTTY
. But if I spawn my nodejs instance using the following command
import cp from 'node:child_process';
const proc = cp.spawn('node', ['-p', 'Boolean(process.stdin.isTTY)']);
process.stdin.pipe(proc.stdin);
proc.stdout.pipe(process.stdout);
proc.stderr.pipe(process.stderr);
await new Promise((ok, err) => proc.once('exit', code => code == 0 ? ok() : err()));
Whereas using { stdio: 'inherit' }
does some magic which causes the filedescriptors to be passed down to the child process, and along with it, the TTY mode.
import cp from 'node:child_process';
const proc = cp.spawn('node', ['-p', 'Boolean(process.stdin.isTTY)'], { stdio: 'inherit' });
await new Promise((ok, err) => proc.once('exit', code => code == 0 ? ok() : err()));
Here, the value true
is printed, whereas above, either false
or undefined
.
This means that there is a difference between piping streams and TTY modes.
My question is specifically about how shells such as BASH or FISH handle this, and what the actual distinction is.
How do child processes inherit this TTY mode, and what happens when the user wishes to pipe one process to another. How do shells which allow you to split panes work? I'm not referring to terminal emulators such as XTerm or Konsole, but the actual shell which can display two simultaneous processes which both seem to be TTY enabled.
Solution
We'll leave the split panes for the moment. Also, don't think about inheriting TTY-modes; in the context of shells that concept doesnot really have any value.
I'll be doing bash, because I don't know fish.
You can test if a shell is interactive with
[[ $- == *i* ]] && echo 'Interactive' || echo 'not-interactive'
If you pipe to a shell, the shell becomes non-interactive:
$ cat | bash
[[ $- == *i* ]] && echo 'Interactive' || echo 'not-interactive'
not-interactive
On the other hand, [ -t 0 ]
tests whether your shell reads from a tty. Consider the following script:
#!/bin/bash
[[ $- == *i* ]] && echo 'Interactive' || echo 'not-interactive'
[ -t 0 ] && echo 'connected to tty' || echo 'not connected to tty'
You can run this in different ways. In the current shell:
$ . test.sh
Interactive
connected to tty
As a script:
$ bash test.sh
not-interactive
connected to tty
or on a pipe:
$ echo | bash test.sh
not-interactive
not connected to tty
or force interactive with -i
:
$ echo | bash -i test.sh
Interactive
not connected to tty
So, what determines if a shell is interactive? According to the source-code:
A shell is interactive if the `-i' flag was given, or if all of
the following conditions are met:
no -c command
no arguments remaining or the -s flag given
standard input is a terminal
standard error is a terminal
During the start-up, bash tests this:
if (forced_interactive || /* -i flag */
(!command_execution_string && /* No -c command and ... */
wordexp_only == 0 && /* No --wordexp and ... */
((arg_index == argc) || /* no remaining args or... */
read_from_stdin) && /* -s flag with args, and */
isatty (fileno (stdin)) && /* Input is a terminal and */
isatty (fileno (stderr)))) /* error output is a terminal. */
init_interactive ();
else
init_noninteractive ();
So, bash determines it at the start-up of the shell.
The point is, that the TTY is not really handled differently. Sure, you can test if the shell is interactive; you can test if the shell reads from a TTY, but that's it. The TTY is just a file descriptor for the shell. In the source code, you can see that there are tests whether STDIN is a TTY, but for the rest: the shell uses STDIN, not a TTY as input. The assumption that TTYs are treated differently is wrong.
Answered By - Ljm Dullaart Answer Checked By - Senaida (WPSolving Volunteer)