7

Say I have a bash script that does:

while :
do
    foo
done

I would like to be able to run this script from the console and be able to exit it at an arbitrary time as long as it happens in between two runs of foo. So if, say, I press Ctrl+C (it could be another action that causes the script to exit, Ctrl+C is just an example) that will exit at the next available point after foo is executed:

while :
do
    foo
    if [pressed_ctrl_c]:
        break
done
5

You could try this sort of construct:

#!/bin/bash
#
INTR=
trap 'INTR=yes; echo "** INTR **" >&2' INT

while :
do
    (
        # Protect the subshell block
        trap '' INT

        # Protected code here
        echo -n "The date/time is: "
        sleep 2
        date
        read -t2 -p 'Continue (y/n)? ' YN || echo
        test n = "$YN" && echo "Asked for BREAK" >&2 && exit 90
    )
    SS=$?
    test 90 -eq $SS && echo "Matched BREAK" >&2 && break

    # Ctrl/C, perhaps?
    test yes = "$INTR" && echo "Matched INTR" >&2 && break
done
exit 0

Some notes

  • The read and test pair demonstrates interactive control to the protected code segment inside the ( ... ) block.
  • The exit 90 is the equivalent of break but from inside a subshell. The test 0 != $? ... line immediately after the subshell block ends is there to capture the exit 90 status and implement the break that the code actually wanted.
  • The subshell can use different exit status values to indicate different types of required control flow (break, exit, etc...)
  • This does not prevent a program installing its own signal handler. For example, gdb installs its own handler for SIGINT (CtrlC). If the aim is to prevent a user breaking out of the session, changing the interrupt key might help obfuscate the situation (see the code below). Inelegant but potentially effective.

Changing the SIGINT key on the terminal

G=$(stty -g)                    # Save settings
test -n "$G" && stty intr ^A    # That is caret and A, not Ctrl/A

# ... SIGINT generated with Ctrl/A rather than Ctrl/C ...

test -n "$G" && stty "$G"       # Restore original settings
  • You haven't addressed the requirement to allow the current invocation of foo to run to completion, even if <Ctrl>+<C> has been typed. – Scott Apr 9 '15 at 17:46
  • Ah, yes. I'll update the code accordingly, thank you @Scott – roaima Apr 9 '15 at 23:03
  • Funny story: That looks a lot like one of my early tries.  I didn’t post it, because it didn’t work — because I typed it into my interactive shell as a one-liner, rather than putting it into a file.  A quick way to test this is to invoke the above script with source rather than the normal way.  I think you’ll find that <Ctrl>+<C> has no effect.  I can’t explain it (can you?)  Friendly suggestion: change your exit 0 to (exit 0) (or simply true), or this will cause your interactive shell to exit.  … … …  BTW, when you said “capture the exit 1 status”, did you mean “… exit 90 status”? – Scott Apr 10 '15 at 0:14
  • @Scott, it's intended to be run as part of a script rather than via source, mainly because if you try to do stuff with trap directly on the command line you can all to easily end up tying yourself in knots. Thank you for exit 1 - I thought I'd caught them all when I switched to 90 (I felt that this new value was "more obvious" as an escape criterion). – roaima Apr 10 '15 at 0:20
  • Yes, I understand that it’s meant to be a script.  I just thought I could debug the script commands faster by doing it on the command line rather than repeatedly editing, saving, and running a script file.  As for tying my shell in knots: I was doing it in parentheses, like (trap "pressed_ctrl_c=1" INT; while true …).  … – Scott Apr 10 '15 at 1:33
4

This seems to work:

#!/bin/sh
pressed_ctrl_c=
trap "pressed_ctrl_c=1" INT
while true
do
        (trap "" INT; foo)&
        wait  ||  wait
        if [ "$pressed_ctrl_c" ]
        then
                # echo
                break
        fi
done
  • Initialize pressed_ctrl_c to null.  This is probably not necessary in a script.
  • trap command signum tells the shell to set up to catch signal number signum and execute command when it catches one.  “INT” is short for “SIGINT” which is short for “interrupt signal”, which is the technical term for the signal generated by Ctrl+C, so, when you type Ctrl+C, the shell sets pressed_ctrl_c to 1.  (SIGINT has a numeric value of 2, so you can say trap "pressed_ctrl_c=1" 2 if you want to economize on typing.)
  • Inside the loop, we have a command line in parentheses.  This creates a sub-shell.
  • We use the trap command again.  This time command is a null string; this tells the shell to ignore signal number signum.  Since this is inside the parentheses, it affects only the subshell.
  • Run foo.  Since it was run from the subshell, foo will ignore Ctrl+C, i.e., it will keep on running even if you type it.
  • Put the subshell in the background ….
  • … and wait for it to complete.
  • If the wait command succeeds, go on to the if statement.  If it fails, execute another one.  (I’ll get back to this.)
  • If $pressed_ctrl_c is set, break out of the loop.  Optionally uncomment the echo command if Ctrl+C appears on your terminal as ^C and you want to move to the next line.

We run a command in the background, and then immediately wait for it.  This is very similar to running the command in the foreground (at least, when done in a script).  The wait command will terminate successfully when the subshell terminates; i.e., when the foo command terminates.  (The wait command will terminate successfully even if foo returns an error.)  When the first wait command terminates successfully, we skip the second one and go to the if.

The shell that’s running the loop is catching interrupts, but the subshell, and thus the foo process, are ignoring them.  So, when you type Ctrl+C, the shell sets pressed_ctrl_c to 1 and aborts the (first) wait command.  Since the first wait command failed, we go on to the second one.  Remember, the foo subshell is still running, so this wait still has something to do (i.e., it will wait for the foo to finish.)

Finally, if the variable has been set to indicate that a Ctrl+C has been pressed while foo was running, break output the loop.

If you press Ctrl+C twice, the second one will interrupt and abort the second wait command.  This will cause your script to terminate and return you to your shell prompt, while leaving foo running in the background.  You can mitigate this by saying wait  ||  wait  ||  wait  ||  wait; extending it as far as you want.  You would need to type Ctrl+C once for each wait to terminate the script prematurely.

D’oh!

A problem with this is that processes that are put into the background by a script have their standard input set to /dev/null.  If your foo reads from the keyboard, the above will need revision.

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.