15

I have a Bash script I was trying to make to help me run a rather complex command with small changes that it would ask me about through echo and read.

I have found solutions to force it to run a terminal to execute the command, but I'm not interested in that. What I would like it to do is, if I space out and just hit Enter on it in Nautilus (making it run with Run Software), it'll just gently pop up a notification saying "Please run this from a terminal."

I can get the popup to happen -- as in I know the command -- but I can't get the Bash script to tell if it's being run inside a terminal or not, it seems to always think so. Is it even possible?

Braiam
  • 35,991
Aescula
  • 363

4 Answers4

12

From man bash under CONDITIONAL EXPRESSIONS:

-t fd  
    True if file descriptor fd is open and refers to a terminal.

Assuming fd 1 is standard out, if [ -t 1 ]; then should work for you. The Advanced Shell Scripting Guide claims that -t used this way will fail over ssh, and that the test (using stdin, not stdout) should therefore be:

if [[ -t 0 || -p /dev/stdin ]]

-p tests if a file exists and is a named pipe. However, I'd note experientially this is not true for me: -p /dev/stdin fails for both normal terminals and ssh sessions whereas if [ -t 0 ] (or -t 1) works in both cases (see also Gilles comments below about issues in that section of the Advanced Shell Scripting Guide).


If the primary issue is a specialized context from which you wish to call the script to behave in a way appropriate to that context, you can sidestep all these technicalities and save your self some fuss by using a wrapper and a custom variable:

!#/bin/bash

export SPECIAL_CONTEXT=1
/path/to/real/script.sh

Call this live_script.sh or whatever and double click that instead. You could of course accomplish the same thing with command line arguments, but a wrapper would still be needed to make point and click in a GUI file browser work.

goldilocks
  • 87,661
  • 30
  • 204
  • 262
  • 5
    this is the correct answer - it is also how POSIX says a shell should detect if it is interactive or not. – mikeserv Oct 18 '14 at 15:26
  • And yet, if I ever want to redirect input to my script, the test now fails, whereas it doesn't with a test for $PS1 being not null. – Franz Kafka Oct 18 '14 at 15:54
  • 2
    @DanielAmaya - if you redirect input then the script is not being run on a terminal. The question is how to detect if the script is being run on a terminal. – mikeserv Oct 18 '14 at 16:02
  • 2
    Are you sure about the use of || within [ … ] like that? If you use [[ … ]] then it would be fine, but normally the || is used to separate commands, and [ -t 0 is an incorrect invocation of [ because its last ] is missing. There typically isn't a command -p either. I agree with testing for a terminal; that's probably the way to do it. It's just the syntax I'm concerned about. – Jonathan Leffler Oct 18 '14 at 16:14
  • Also, whether or not /dev/stdin exists in the file system depends on the operating system. – chepner Oct 18 '14 at 16:40
  • -p is a POSIX-specified primary for the test command. – chepner Oct 18 '14 at 16:42
  • @chepner: agreed, but the context is that the -p is after the || operator that separates commands. – Jonathan Leffler Oct 18 '14 at 17:34
  • 1
    @JonathanLeffler Right; that should produce a syntax error, since the shell operator || is seen before the required final ] argument to [. – chepner Oct 18 '14 at 17:44
  • This does not work in csh. – Franz Kafka Oct 18 '14 at 17:56
  • @DanielAmaya - very true. csh doesn't handle a lot of the in/out stuff inherited from the traditional Bourne style shells. On the other hand, the tradtional Bourne-style shells didn't handle the history and job control offered in csh. general consensus was eventually found with ksh - and that is why it is the basis for the POSIX sh standard. – mikeserv Oct 18 '14 at 18:06
  • @mikeserv, and yet this question relates to bash, and from bash's very own man page we see the answer to this question being the two answers you've objected to here. Your objections are not based on bash, but rather sh. So, I figure if we can throw any shell out there, we may as well throw csh into the mix. The point being, none of these answers are reliable, as I've stated above. – Franz Kafka Oct 18 '14 at 18:08
  • @DanielAmaya - bash does the same as most modern shells - it detects automatically if it should start interactive based on whether its i/o is connected to a terminal. It is forced interactive with -i. Testing for an interactive shell is not the question - the question is testing for connection to a terminal. – mikeserv Oct 18 '14 at 18:11
  • @mikeserv, I think it's as simple as providing an example using bash that proves mine and xae's answers are not reliable (as you've stated). – Franz Kafka Oct 18 '14 at 18:14
  • @DanielAmaya - fair enough. Try running: printf %b\\n 'PS1="this is an interactive shell\n"' '[ -t 0 ] || [ -t 1 ] || exit' | bash -i 2>&1 | sed '$a\and it is not connected to a terminal' – mikeserv Oct 18 '14 at 18:25
  • @JonathanLeffler Thx -- replaced with double squares. – goldilocks Oct 18 '14 at 18:43
  • @DanielAmaya The Q is tagged bash. Pragmatically, yours is perhaps the better test (+1) but not perfect; I wanted to post this because there is something canonical about it (considering the specifics of the question) and its use value for people who may find their way here. – goldilocks Oct 18 '14 at 18:49
  • 1
    @goldilocks Daniel's answer is wrong, plain and simple. PS1 is often set in a shell that isn't interactive, because people often set it in .profile or /etc/profile. – Gilles 'SO- stop being evil' Oct 18 '14 at 20:31
  • 5
    That section from the Advanced Bash-Scripting Guide has several errors. PS1 is not a reliable test to tell whether the shell is interactive. “If a script needs to test whether it is running in an interactive shell” is also confusing: it should be if some code needs to test — a script is usually not running in an interactive shell (but it can be, if it's sourced). Testing for i in $- is the correct way to test if the shell is interactive. Testing -t 0 or -t 2 is the correct way to tell if the script is running in a terminal, which is different from being interactive. – Gilles 'SO- stop being evil' Oct 18 '14 at 20:37
  • 1
    As for the -p bit, it contradicts the comment above that mentions a socket, for which the test would be -S. But I don't know what John Lange is trying to do there: if stdin isn't a terminal over SSH, it means that the script is not executed interactively, in which case it probably shouldn't do whatever it likes to do in a terminal, and in particular it shouldn't try to perform any user interaction. – Gilles 'SO- stop being evil' Oct 18 '14 at 20:39
  • Gilles, actually I will agree with you on this, and I think given the question, the best answer to it would have to be the one that is most downvoted here (testing for i in $-).

    Why? It's pretty clear that OP does not want to be able to double-click the script in Nautilus, and would rather it be run in the terminal (so that they can interact with it). However, if OP decides to redirect input for some reason at a later time, then the script breaks.

    – Franz Kafka Oct 18 '14 at 22:30
  • @Gilles, Actually, no I think I will nitpick on this some more. "An interactive shell is one started without non-option arguments and without the -c option whose standard input and error are both connected to terminals (as determined by isatty(3)), or one started with the -i option. PS1 is set and $- includes i if bash is interactive, allowing a shell script or a startup file to test this state.

    The following paragraphs describe how bash executes its startup files."

    – Franz Kafka Oct 18 '14 at 22:37
  • @Gilles, continuing on: "When bash is invoked as an interactive login shell, or as a non-interactive shell with the --login option, it first reads and executes commands from the file /etc/profile, if that file exists."

    So, in essence it would seem that both you and mikeserv disagree with the bash man page on this one.

    – Franz Kafka Oct 18 '14 at 22:38
  • This as written SEEMS to work... But the strange thing is, I'm not getting the notifications to work when I run it from Nautilus. Either way. The cursor turns into the "working" thing and then back, nothing happens. – Aescula Oct 19 '14 at 00:38
  • 1
    @DanielAmaya No, you're misreading the man page, because you're confusing interactive shell with shell running in a terminal. A script (executed as a standalone program, through #!/bin/bash) is never interactive, even if it's running in a terminal. The remark about PS1 is admittedly misleading: bash sets it in an interactive shell, but it can also be set in a non-interactive shell, so it isn't a reliable test. – Gilles 'SO- stop being evil' Oct 19 '14 at 10:10
  • @Aescula This answer does what you requested in the question. If your notification mechanism doesn't work, you may want to ask another question, where you describe the notification mechanism that you used. But why don't you just start a terminal emulator and run the script in it? – Gilles 'SO- stop being evil' Oct 19 '14 at 10:12
  • Aescula, I've added an idea to the bottom if your problem is specifically to do with invoking via point and click in a GUI. @Gilles Thanks for the comments, I've added a further caveat about that section of the "Advanced Guide". – goldilocks Oct 19 '14 at 13:20
  • @Gilles I don't want to emulate it because it asks me for input every time. It streamlines a command, but it's a find. I don't want to find the same regex each time. Since I have narrowed down the problem to being the notifications, this is honestly less of an issue. If it becomes more of a problem I will open a new question. Thank you all! – Aescula Oct 20 '14 at 11:07
3

Use the bash $SHLVL variable to detect the level of shell nesting. In a script run 'raw' by double-clicking it will be 1, in a script running within a terminal it will be 2.

#!/bin/bash
if (( SHLVL < 2 )) ; then
    echo "Please run this from a terminal."
    read -p "Press <Enter> to close this window"
    exit 1
fi
# rest of script

Note: $ is not required when testing numerical variables within (( )).

1

Although goldilocks' answer is probably correct in the typical case, it does seem that there are edge cases. In my own case, my xserver is configured to start up from tty1 and it never leaves that tty. If Xorg's stdout is a TTY then it seems the clients will have that TTY linked to their file descriptor by default.

Here's how I solved my issue:

#!/bin/bash
isxclient=$( readlink /dev/fd/2 | grep -q 'tty' && [[ -n $DISPLAY ]] ; echo $? )
if [[ ! -t 2  || $isxclient == "0" ]]; then
        notify-send "Script wasn't started from an interactive shell"
else
        echo "Script was started from an interactive shell"
fi

I haven't tested this to see if it works on a more standard X configuration, and I also doubt very much that this is the only edge case. If anyone finds a more generally applicable solution, please come back and tell us.

AdminBee
  • 22,803
JMW
  • 131
-2

Another, using the bash options set internal variable, $-.

From .bashrc,

# If not running interactively, don't do anything
case $- in
    *i*) ;;
    *) return;;
esac
xae
  • 2,021
  • an interactive shell isnt necessarily connected to a terminal. while one started with that connection is automatically started interactive, this also is possible: cmd | sh -i | cmd. – mikeserv Oct 18 '14 at 15:30
  • This code is being executed in a script. It won't be interactive, even if it is running in a terminal. – Gilles 'SO- stop being evil' Oct 18 '14 at 20:29