6

I have a bash script from which I want to access /dev/tty, but only when it's available.

When it's not available (in my case: when running my script in GitHub Actions) then when I try to access it I get /dev/tty: No such device or address, and I'm trying to detect that in advance to avoid the error and provide fallback behaviour instead.

To do so I need a bash test that can detect cleanly this case, and which will work reliably across platforms (i.e. not using the tty command, which has issues on Mac).

I'm currently using [[ -e "/dev/tty" ]] which doesn't work - it appears to return true even on GitHub Actions, where it seems that /dev/tty exists but accessing it will fail. What should I use instead?

Tim Perry
  • 11,766
  • 1
  • 57
  • 85
  • Would [this page of the bash manual](https://www.gnu.org/software/bash/manual/html_node/Is-this-Shell-Interactive_003f.html) be useful? – ErikMD Sep 06 '21 at 14:04
  • No idea why, but `$-` contains "hBH" in my bash, not `i`, so that example doesn't work. – Tim Perry Sep 06 '21 at 14:18
  • For what purpose does the bash script want to access /dev/tty ? – Philippe Sep 06 '21 at 15:46
  • @Philippe to launch $EDITOR connected to the TTY, from inside a pipeline of other commands (so no direct access to stdin/stdout). When /dev/tty is available that works great, and when it's not I can provide an OK fallback, but I need to be able to detect that. – Tim Perry Sep 06 '21 at 18:31

5 Answers5

7

After testing lots of promising but not quite perfect suggestions (see the other answers), I think I've found my own solution that does exactly fit my needs:

if sh -c ": >/dev/tty" >/dev/null 2>/dev/null; then
    # /dev/tty is available and usable
else
    # /dev/tty is not available
fi

To explain:

: >/dev/tty does nothing (using the : bash built-in) and outputs the nothing to /dev/tty, thereby checking that it exists & it's writable, but not actually producing any visible output. If this succeeds, we're good.

If we do that at the top level without a /dev/tty, bash itself produces a noisy error in our output, complaining about /dev/tty being unusable. This can't be redirected and silenced because it comes from bash itself, not the : command.

Wrapping that with sh -c "..." >/dev/null 2>/dev/null runs the test in a bash subshell, with stdout/stderr removed, and so silences all errors & warnings while still returning the overall exit code.

Suggestions for further improvements welcome. For reference, I'm testing this with setsid <command>, which seems to be a good simulation of the TTY-less environment I'm having trouble with.

Jack G
  • 4,553
  • 2
  • 41
  • 50
Tim Perry
  • 11,766
  • 1
  • 57
  • 85
1

Try this approach :

if  test "$(ps -p "$$" -o tty=)" = "?"; then
    echo "/dev/tty is not available."
else
    echo "/dev/tty is available."
fi
Philippe
  • 20,025
  • 2
  • 23
  • 32
  • I've done some testing and this seems to work pretty well! I've upvoted but I'm going to use my own slightly simpler answer below that just uses printf & bash instead, just because I'm slightly worried that complex ps commands like this might work slightly differently on Mac and other weird environments (see https://apple.stackexchange.com/questions/300864/how-to-get-the-basic-linux-ps-functionality-in-mac for example). – Tim Perry Sep 07 '21 at 12:28
  • So running via cron, I got a zero exit status with the output `??`. On my mac terminal I get `ttys004` as the output. So I think this test needs to be adjusted to check for any ? rather than a single question mark. Other than that, this was the only reliable way of detecting it. – balupton Aug 15 '23 at 16:32
  • Here is an updated variation `[[ "$(ps -p "$$" -o tty=)" != '?'* ]]` – balupton Aug 15 '23 at 16:35
1

Instead of spawning a new shell process to test if /dev/tty can really be opened for writing (test -w lies, you know?), you can try to redirect stdout to /dev/tty from a subshell like so:

if (exec < /dev/tty) ; then
  # /dev/tty is available
else
  # no tty is available
fi

This is POSIX syntax and should work in any shell.

Jack G
  • 4,553
  • 2
  • 41
  • 50
0

It seems that adapting this answer from this question on ServerFault (entitled How can I check in bash if a shell is running in interactive mode?, which is close to your question albeit not an exact duplicate) could be a solution for your use case.

So, could you try writing either:

  • [ -t 0 ] && [ -t 1 ] && echo your code
  • or [ -t 0 ] && echo your code ?

For completeness, here is one link documenting this POSIX flag -t, which is thus portable:

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html

-t file_descriptor
True if file descriptor number file_descriptor is open and is associated with a terminal.
False if file_descriptor is not a valid file descriptor number, or if file descriptor number file_descriptor is not open, or if it is open but is not associated with a terminal.

Furthermore, if you use bash (not just a POSIX-compliant shell), you might want to combine this idea with the special 255 file descriptor number: [ -t 255 ].

Source: On Unix&Linux-SE,

That 255 file descriptor is an open handle to the controlling tty and is only used when bash is run in interactive mode. […]

In Bash, what is file descriptor 255 for, can I use it? (by @mosvy)

ErikMD
  • 13,377
  • 3
  • 35
  • 71
  • In my case, stdin & stdout are pipes to other processes, so -t 0 & -t 1 are false, and this won't work. I still want to communicate with the controlling TTY when one is available though, by using /dev/tty directly. When /dev/tty is available this works fine! But not in completely TTY-less environments. – Tim Perry Sep 06 '21 at 14:16
  • OK but do you think you could adapt my suggestion and "keep" the stdin available via another FD number, just for this test purpose? – ErikMD Sep 06 '21 at 14:18
  • How? Can you give an example? This script is run as part of a pipeline, so it never has any access to the TTY via stdin or any FD, so I can't "keep" it. – Tim Perry Sep 06 '21 at 14:22
  • For example, if you use `bash` (not just a POSIX shell), I've just seen in [this SE thread](https://unix.stackexchange.com/q/475389/297058) that the special FD number 255 (to make available `/dev/tty` even if stdin/stdout have been redirected) might be a solution for you to test (namely, `[ -t 255 ]`…) – ErikMD Sep 06 '21 at 14:44
  • FD 255 is an option, that might work. It would be better to have an option to test /dev/tty itself though, without being dependent on bash internal implementation details. I'd really prefer to check the state of the tty device I want to use directly, rather than patch in another workaround. – Tim Perry Sep 06 '21 at 18:36
  • @TimPerry yes, this definitely sounds like a workaround :'-) but which might work on Mac also, thanks to the availability of `bash`… (let us know if ever you can test this) – ErikMD Sep 06 '21 at 22:26
  • I tested this, and it works in some cases, but not all cases. When running `bash ./test.sh` in an interactive terminal for example `[ -t 255 ]` is incorrectly false. – Tim Perry Sep 07 '21 at 12:22
  • Indeed, but this does not look wrong to me − the fact that `[ -t 255 ] && echo true` is false with `bash ./test.sh` → it would certainly print true with `bash -i ./test.sh`; but maybe this is not exactly the criterion you want to test. – ErikMD Sep 07 '21 at 13:26
  • Yes - that's not the right criterion. I'm only interested in whether /dev/tty is available (and it is by default when you run any bash script in an interactive terminal, with or without -i). – Tim Perry Sep 07 '21 at 13:28
0

Beyond the other answers mentioned in this thread (and as an alternative to the other idea involving $-, which did not seem to work for you), what about this other idea mentioned in the bash manual?

if [ -z "$PS1" ]; then
    echo This shell is not interactive
else
    echo This shell is interactive
fi
ErikMD
  • 13,377
  • 3
  • 35
  • 71
  • Good suggestion, but this doesn't seem to work reliably unfortunately... In my testing, when running a saved script in bash (v5) then $PS1 is unset, even if it's set in the terminal itself & /dev/tty is working (also: this actually needs to be ${PS1+x} to handle the case where it's set but empty). – Tim Perry Sep 07 '21 at 10:56