4

I would like to implement a zsh script that behaves differently depending on whether it was invoked directly on the shell's CLI or not.

I thought at first that the script could do this by looking for i in the value of $-, but I was wrong.

In fact, when I run the script below from the command-line

#!/bin/zsh
printf -- '%s\n' "$-"

...the output I get does not include i1.

Is there some other way for my shell script to figure out this information?

NB: Although I am working on a zsh script at the moment, I would also like to know what the answer would be for a bash script. If the answer depends on the OS, I'm primarily interested in Linux and Darwin.


1The script's output is 569X, if anyone cares to know.

10
  • 2
    Is this the same as asking "does the script have a controlling terminal?", if so, [ -t 1 ] tells you whether standard output is a terminal. Not an answer as I don't quite understand your context, and there is no example.
    – Kusalananda
    Oct 6, 2021 at 14:45
  • 2
    Does this answer your question? How to check if a shell is login/interactive/batch. Seems to have good options for a number of shells. The zsh one is a ways down the page, but listed. Oct 6, 2021 at 14:57
  • 2
    Your question's title and first sentence ask subtly different things. One asks about invoking it interactively and the other asks about invoking it directly from the shell's command line. As a trivial example, cat | myprogram | cat won't be interactive, but it's started from the command line Oct 6, 2021 at 15:31
  • 1
    The zsh process running your script is non-interactive. Whether the parent process is an interactive shell is a separate matter that won't be reflected in $_. Checking for a controlling terminal as suggested in the first comment is the normal way to solve this problem.
    – okapi
    Oct 6, 2021 at 16:43
  • 1
    @roaima: Thank you for clarifying that confusion. I have changed the title of my post to bring it in line with my post's first line.
    – kjo
    Oct 6, 2021 at 20:29

1 Answer 1

6

$- contains i when the shell itself is an interactive shell, when it issues prompts, lets you enter and edit command lines on the terminal, does job control, etc.

A script is typically interpreted by a non-interactive shell. If you invoke the script as ./the-script or zsh ./the-script, that zsh instance is not interactive. The only time your script will be interpreted by an interactive shell is when you tell your interactive shell to interpret the code in it with things like source ./the-script or eval "$(<the-script)".

Here, it seems to me you rather want to know if the script is invoked from within a terminal, like you want to tell whether a user can interact with it through a terminal.

That the [ / test command (which is builtin in zsh and bash and most other Bourne-like shells, but should also exist as a standalone command on POSIX systems) can tell you with its -t operator. That -t operator takes one¹ numerical argument which is a file descriptor.

[ -t 0 ] or test -t 0 returns true if the file descriptor 0 is open on a terminal device. File descriptor 0 is standard input, 1 is standard output, 2 is standard error, all other fds don't have any reserved special meaning.

So in your script, you can do:

if [ -t 0 ]; then
  echo "I (this script) am taking input from a terminal, so likely from a user"
fi

if [ -t 1 ]; then
  echo "My output goes to a terminal device, so will likely be seen by a user"
  [ -t 2 ] && echo errors also go there.
fi

If you want to check that the script is run from within a terminal session even if its input and/or output may be redirected to some other file than the terminal device, you can check whether there's a terminal attached to the script's session with something like:

if { true <> /dev/tty; } 2> /dev/null; then
  echo "there's a terminal device attached to my session"
fi

If you want to know what terminal device it is:

tty_on_stdin=$(tty)
tty_on_stderr=$(tty <&2)
{ tty_on_stdout=$(tty <&3); } 3>&1

(see this follow-up Q&A for the reason behind that little dance with fd 3).

controlling_tty=$(LC_ALL=C ps -o tty= -p "$$")
[ "$controlling_tty" = '?' ] || controlling_tty=/dev/$controlling_tty

¹ In earlier test / [ implementations, that argument was optional and [ -t ] was short for [ -t 1 ]. That is no longer allowed by POSIX as that conflicts with the [ "$var" ] use-case which is short for [ -n "$var" ]. Still, you'll find that ksh93 still checks whether stdout goes to a tty with [ -t ] though only when -t is literal; var=-t; [ "$var" ] returns true even if stdout is not on a terminal.

1
  • Thank you for this terrific post! The difference between the expressions for getting the values for tty_on_stderr and tty_on_stdout is surprising/puzzling to me. I think it merits its own question.
    – kjo
    Oct 7, 2021 at 11:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .