1

I'm writing a shell script :

#!/bin/bash

i=1;
sp="/-\|";
no_config=true;
echo "type \"continue\" to exit this while loop";
echo "if you feel the conditions for continuing sucessfully have been met... ";
while $no_config;
do printf "\b${sp:i++%${#sp}:1}";
[[ ! $(pidof SupremeCommande) && -f ~/My\ Documents/newfile ]] && no_config=false;
sleep 1;
done;
# do more stuff

I'd like to now add a user-provoked exit out of the loop as well.

this seems complicated.

at first I found this and my hopes were up : https://unix.stackexchange.com/a/228152/228658 but as it turns out the read command halts execution.

I don't want that. I want something like man pages where if at any time you hit "q" it exits but it isn't halting the ability to scroll up and down. it is waiting for input in the background discreetly.

how would I accomplish that in a shell script?

2

A simpler solution would be to inform the user to press Ctrl-C to exit the loop, which would issue the SIGINT signal and end the script.

If you want to do something else before ending the script, you can trap the signal:

cleanup() {
  # ...
  exit 0
}
trap 'cleanup' INT

Alternatively, you can run one of them in a subshell, making sure that when one of the shells exits, the other is killed as well. This way you can have one of them blocked for input while the other does the other checks.

#!/bin/bash

i=1
sp="/-\|"
no_config=true
echo "type \"continue\" to exit this while loop if you feel the conditions for continuing sucessfully have been met... "
(
while $no_config
do 
  printf "\b${sp:i++%${#sp}:1}"
  [[ ! $(pidof SupremeCommande) && -f ~/My\ Documents/newfile ]] && no_config=false
  sleep 1
done
kill $$
) &
child_pid=$!

while $no_config
do
  read -r typed_continue
  [[ "$typed_continue" = "continue" ]] && no_config=false
  sleep 1
done
kill $child_pid
  • wow subshell actually doesn't open another terminal. what is this back magic? also why set -eu and why /usr/bin/env bash I did not use those two? – tatsu Apr 24 '19 at 11:45
  • Those changes are optional, I've reverted them. I also added another alternative if you feel the subshell solution is too complex for your needs. – fzbd Apr 24 '19 at 11:51
  • no it acutally works. I don't understand how but it accomplishes what I want, thanks! is it POSIX? – tatsu Apr 24 '19 at 11:52
  • 1
    Subshells are POSIX: pubs.opengroup.org/onlinepubs/007904975/utilities/…, basically they are sub processes that do their own parallel processing. You can read more about it here: tldp.org/LDP/abs/html/subshells.html – fzbd Apr 24 '19 at 12:08
  • I'm having trouble having this code inline of a terminal tab, by which I mean the whole set of commands are between quotes and each line has a ; at the end. this worked file for the while loop above but the parentheses are causing issues. what would be the syntax? – tatsu Apr 24 '19 at 15:12
2

Unfortunately Bash does not lend itself nicely to such controls.

However there can be some ways to obtain something workable.

From the simplest to the most complex, here are some:

Type Ctrl+C to exit

You can install a trap for Ctrl+C (handled by catching the signal INT) to make it set your no_config value to false so that your while loop quits on Ctrl+C:

i=1
sp='/-\|'
no_config=true
echo "type Ctrl+C to exit this while loop"
echo "if you feel the conditions for continuing successfully have been met... ";
trap 'no_config=false' INT
while $no_config; do
    printf "\b${sp:i++%${#sp}:1}"
    [[ ! $(pidof SupremeCommande) && -f ~/My\ Documents/newfile ]] && no_config=false
    sleep 1
done
trap 'trap - INT; kill -INT $$' INT
echo "do more stuff"

(If you run the above code from an interactive shell remember to surround the whole code with a leading ( and a trailing ) so that your shell won't be polluted by what the code does.)

Note that the while loop will actually be interrupted only when all commands inside the loop have completed, because the test on $no_config occurs only at the while. You may or may not want this behavior depending on what you do inside the loop. In your example case the only true “slow” operation is the explicit sleep 1, so an abrupt interruption shouldn’t be necessary, but if you do want to interrupt the loop no-matter-what then the easiest addendum is by using one sub-shell to be killable as a whole. Like this:

trap 'kill $!' INT
(
i=1
sp='/-\|'
no_config=true
echo "type Ctrl+C to exit this while loop"
echo "if you feel the conditions for continuing successfully have been met... ";
while $no_config; do
    printf "\b${sp:i++%${#sp}:1}"
    [[ ! $(pidof SupremeCommande) && -f ~/My\ Documents/newfile ]] && no_config=false
    sleep 1
done
) &
wait
trap 'trap - INT; kill -INT $$' INT
echo "do more stuff"

Here we set a trap for INT (Ctrl+C) which kills the sub-shell, so that the wait proceeds to the “do more stuff” part.

Note however that, by doing this way, variables set within the loop do not propagate to the "do more stuff" part. To work around this you need some ancillary communication like a temporary file or named-FIFO.


Type 'q' to exit

An alternative to the above that would allow you to use any character to quit (thus also leaving Ctrl+C to its regular operation) might be by embedding a non-blocking read (available on Bash v4+) within the loop. This also requires some ancillary preparations for terminal settings. The whole thing might be like this:

term_settings="$(stty -g)"
trap 'stty "${term_settings}"' EXIT
i=1
sp='/-\|'
no_config=true
echo "type exactly \"q\" to exit this while loop"
echo "if you feel the conditions for continuing successfully have been met... ";
stty -icanon -echo
while $no_config; do
    while read -t 0 && read -rN 1 ; do [ "${REPLY}" = q ] && break 2 || true; done
    printf "\b${sp:i++%${#sp}:1}"
    [[ ! $(pidof SupremeCommande) && -f ~/My\ Documents/newfile ]] && no_config=false
    sleep 1
done
stty "${term_settings}"
echo "do more stuff"

Here we first save the terminal’s current settings to restore them later and on EXIT, then disable displaying of typed character (-echo) and line-oriented keyboard input (-icanon). Within the while loop we use read -t 0 (non-blocking read) to check for presence of typed characters, in which case we read them all one by one (-N 1) to test whether any one of them is q to quit the loop.

This adds a bit of computational overhead due to the read -t 0 done at each iteration of your while loop. In your example case the overhead is negligible.


If you wish an abruptly interruptible loop with a type q to quit, a different approach is necessary, and things get much more complicated because you have to resort to running the interruptible part of your code in background while waiting on a read in a foreground code, and this also requires a way to unblock the read when the background code completes regularly.

Obtaining this requires a lot of stretching Bash’s abilities, for instance by:

  1. enclosing the interruptible part of your code in a background sub-shell on its own, a bit like we’ve done earlier but with additional cares taken
  2. simulating a Ctrl+C from the background sub-shell to the foreground read.

This also means that it won't be possible to pass variables from the while loop to the "do more stuff" part, just like we said earlier (an ancillary communication would need to be set up on purpose).

An example might be like this:

#!/bin/bash

term_settings="$(stty -g)"
(
trap 'stty "${term_settings}"' EXIT
_keyboard_waiter=$BASHPID
(
i=1
sp='/-\|'
no_config=true
echo "type exactly \"q\" to exit this while loop"
echo "if you feel the conditions for continuing successfully have been met... ";
while $no_config; do
    printf "\b${sp:i++%${#sp}:1}"
    [[ ! $(pidof SupremeCommande) && -f ~/My\ Documents/newfile ]] && no_config=false
    sleep 1
done
kill -INT $_keyboard_waiter
) &
while read -rsN 1 && ! [ "${REPLY}" = q ] ; do :; done
{ kill $! && wait; } 2>/dev/null
)
echo "do more stuff"

Here we have:

  • better used the whole thing through a script file, due to the required handling of signals
  • terminal settings saved at the beginning to restore them later on EXIT, which is necessary for when read gets interrupted
  • one sub-shell run in foreground, responsible for waiting input from keyboard
  • one sub-sub-shell run in background (&) for the interruptible part of the code
  • use of kill -INT by the background sub-shell to interrupt the read's sub-shell. This simulates Ctrl+C for that sub-shell, which is handled by Bash
  • an option-ed read to not show typed characters (-s) while also waiting/accepting just one character regardless of newlines (-N 1), plus the common -r to not interpret backslashes as escape character
  • a simple “kill&wait” to the background sub-shell for when ‘q’ is indeed typed
  • 1
    what a very complete and pedagogue answer! I'm liking the middle solution especially! I'm going to test it and get back to you! – tatsu Apr 26 '19 at 14:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.