Wednesday, February 7, 2024

[SOLVED] How to detect when a bash script is triggered from keybinding

Issue

Background

I have a Bash script that requires user input. It can be run by calling it in a terminal or by pressing a keyboard shortcut registered in the i3 (or Sway) config file as follows:

bindsym --release $mod+Shift+t exec /usr/local/bin/myscript

The Problem

I know I can use read -p to prompt in a terminal, but that obviously won't work when the script is triggered through the keybinding. In that case I can use something like Yad to create a GUI, but I'm unable to detect when it's not "in a terminal". Essentially I want to be able to do something like this:

if [ $isInTerminal ]; then
    read -rp "Enter your username: " username
else
    username=$(yad --entry --text "Enter your username:")
fi

How can I (automatically) check if my script was called from the keybinding, or is running in a terminal? Ideally this should be without user-specified command-line arguments. Others may use the script and as such, I'd like to avoid introducing any possibility of user error via "forgotten flags".


Attempted "Solution"

This question suggests checking if stdout is going to a terminal, so I created this test script:

#!/usr/bin/env bash

rm -f /tmp/detect.log

logpass() {
    echo "$1 IS opened on a terminal" >> /tmp/detect.log
}
logfail() {
    echo "$1 IS NOT opened on a terminal" >> /tmp/detect.log
}

if [ -t 0 ]; then logpass stdin; else logfail stdin; fi
if [ -t 1 ]; then logpass stdout; else logfail stdout; fi
if [ -t 2 ]; then logpass stderr; else logfail stderr; fi

However, this doesn't solve my problem. Regardless of whether I run ./detect.sh in a terminal or trigger it via keybind, output is always the same:

$ cat /tmp/detect.log
stdin IS opened on a terminal
stdout IS opened on a terminal
stderr IS opened on a terminal

Solution

False Positive

As most results on Google would suggest, you could use tty which would normally return "not a tty" when it's not running in a terminal. However, this seems to differ with scripts called via bindsym exec in i3/Sway:

/dev/tty1  # From a keybind
/dev/pts/6 # In a terminal
/dev/tty2  # In a console

While tty | grep pts would go part way to answering the question, it is unable to distinguish between running in a console vs from a keybinding which you won't want if you're trying to show a GUI.


"Sort of" Solution

Triggering via keybinding seems to always have systemd as the parent process. With that in mind, something like this could work:

{
    [ "$PPID" = "1" ] && echo "keybind" || echo "terminal"
} > /tmp/detect.log

It is likely a safe assumption that the systemd process will always have 1 as its PID, but there's no guarantee that every system using i3 will also be using systemd so this is probably best avoided.


Better Solution

A more robust way would be using ps. According to PROCESS STATE CODES in the man page:

For BSD formats and when the stat keyword is used, additional characters may be displayed:

<    high-priority (not nice to other users)
N    low-priority (nice to other users)
L    has pages locked into memory (for real-time and custom IO)
s    is a session leader
l    is multi-threaded (using CLONE_THREAD, like NPTL pthreads do)
+    is in the foreground process group

The key here is + on the last line. To check that in a terminal you can call ps -Sp <PID> which will have a STAT value of Ss when running "interactively", or S+ if it's triggered via keybinding.

Since you only need the STATE column, you can further clean that up with -o stat= which will also remove the headers, then pipe through grep to give you the following:

is_interactive() {
    ps -o stat= -p $$ | grep -q '+'
}

if is_interactive; then
    read -rp "Enter your username: " username
else
    username=$(yad --entry --text "Enter your username:")
fi

This will work not only in a terminal emulator and via an i3/Sway keybinding, but even in a raw console window, making it a much more reliable option than tty above.



Answered By - Dave S
Answer Checked By - Pedro (WPSolving Volunteer)