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.

kjo
  • 15,339
  • 25
  • 73
  • 114
  • 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 06 '21 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. – NotTheDr01ds Oct 06 '21 at 14:57
  • Does this answer your question? shebang or not shebang – Todd Walton Oct 06 '21 at 15:00
  • 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 – Chris Davies Oct 06 '21 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 06 '21 at 16:43
  • “Interactive” has a specific technical meaning when it relates to shell, but judging by your question, I think you're using a different meaning. “Invoked interactively” means that the shell will do things like reading an rc file, displaying a prompt, enabling command line editing and history, etc., which it doesn't do when non-interactive. Usually a shell is only interactive if it was started without telling it to run a script. It's possible to run an interactive shell on a script (sh -i myscript), but unusual. What do you mean by “invoked interactively”? – Gilles 'SO- stop being evil' Oct 06 '21 at 18:33
  • 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 06 '21 at 20:29
  • @Gilles'SO-stopbeingevil': sorry for my original post's poor wording. Where I wrote "interactively", I really should have written "from the shell's CLI". I changed the title accordingly. I hope that this change answers your question. – kjo Oct 06 '21 at 20:31
  • 1
    Do you have a particular reason for the script to determine how it was started? Depending on the end-goal, there might be other ways forward. Also, do you have some other particular situation you're thinking the script might be launched in, when not launched from an interactive shell? Is it something that would often run from cron, or a service, or ...? – ilkkachu Oct 06 '21 at 20:35
  • @ilkkachu: I do have a reason, but the situtation where it arises is rather complicated. If it turns out that it is harder than I expected for a script to know how it was invoked, then I will follow your advice, and look for a different way forward. – kjo Oct 06 '21 at 21:47

1 Answers1

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.

  • 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 07 '21 at 11:03