0

I'm writing a script that will fetch a list of database names from a server so the user knows which databases they can enter in the next prompt:

echo "FETCHING DB NAMES... (^C to skip)"
ssh "$user@$SERV_A" "$remote_cmd" #returns space-delim db list
echo
printf "Which db do you want to import? >"
read db_name
#rest of script

It looks like this to the user:

FETCHING DB NAMES... (^C to skip)
db1 db2 personnel

Which db would you like to import? >

The problem is that fetching those databases happens can take between 1-10 seconds depending on the connection. If the user already knows the db name, this can be frustrating.

Of course, pressing ^C as the prompt indicates kills the entire script rather than just the ssh process. Is there a way I can write the script so they can cancel/skip just that ssh process?

Thank you in advance.

EDIT: it doesn't have to be ^C per se, really just want to give the user a way to skip a running child process.

James M. Lay
  • 2,270
  • 25
  • 33
  • I wouldn't recommend `control-c` as the means to skip, although using `s`, `q`, or whatever else would probably be ok... – l'L'l Nov 19 '16 at 01:41
  • Excellent point. I updated the question. – James M. Lay Nov 19 '16 at 01:46
  • @JamesM.Lay Have made a couple updates to initial answer to take into account the input block, and skipping over the whole thing if the user wants to skip the ssh part – hmedia1 Nov 19 '16 at 03:05
  • Also see [Kill bash script foreground children when a signal comes](http://stackoverflow.com/q/4261726), [How to propagate a signal through an collection of scripts?](http://stackoverflow.com/q/2525855) and [Forward SIGTERM to child in Bash](http://unix.stackexchange.com/q/146756) on Unix & Linux Stack Exchange. – jww Nov 19 '16 at 12:48
  • It really seems like you're doing this backward. Instead of playing around with signals to terminate the ssh, just don't do the ssh unless you must. That is, let the user enter the db name as a parameter to the script and only run the ssh if they leave it blank. (Or, less correct in my opinion, prompt for the name and allow the user to request a list, then run the ssh to get the list) – William Pursell Nov 20 '16 at 00:52

2 Answers2

2

Here is a possible way using trap on SIGQUIT (Ctrl+\)

(For this example I am substituting your ssh command for a simple sleep 5 ; echo "names" to simulate an output from ssh)

#!/bin/bash
bashpid="$$"
trap "kill -9 "$bashpid"" SIGINT

echo "FETCHING DB NAMES... (^\ to skip)"         # Fetching text

getdbnames () {
sleep 5 ; echo "names"                           # ssh command
}

dbnames="$({ getdbnames &
trap "kill -9 "$!"" SIGQUIT 
wait
})"                                              # uses trap and wait to handle Ctrl+\

trap "" SIGQUIT                                  # Avoid accidental double Ctrl+\

if [[ -n "$dbnames" ]]; then                     # Only ask import if dbnames variable
    printf "Which db do you want to import? >"   # isn't empty, otherwise
    read db_name                                 # skip this whole
fi                                               # section 
echo "Rest of script"

There are quite a few other ways, including looping read statements for input, and using SIGCHLD and/or other internals that may or may not require certain shell options present. For the sake of skipping one task, this might be reliable.

This also traps ctrl+c to ensure that it kills the entire script (Sometimes scripts produce what you're after as an unwanted effect, and sometimes playing around with subshell methods and traps can cause it), so this is just to ensure that:

  • Ctrl+C kills everything
  • Ctrl+\ kills the subtask

Running the script above and pressing Ctrl+C:

  • FETCHING DB NAMES... (^\ to skip)
    ^CKilled: 9

Running the script above and pressing Ctrl+\:

  • FETCHING DB NAMES... (^\ to skip)
    ^\Rest of script

Running the script above and leaving it to run (a successful ssh result)

  • FETCHING DB NAMES... (^\ to skip)
    names
    Which db do you want to import? >

Obviously there would be some more refining to tune it to your exact environment, but the boilerplate is there.

hmedia1
  • 5,552
  • 2
  • 22
  • 27
  • The only thing that I don't understand is that you set the trap after the ssh invocation. Wouldn't the script terminate before the trap is evaluated? – James M. Lay Nov 19 '16 at 21:56
  • @JamesM.Lay An oversight of mine after altering the script to account for the conditional skip of the read block. Code updated, and also improved by using the blank trap/ignore technique in Mark Plotnicks answer to avoid accidentally pressing ctrl+\ twice and running the risk of sending an actual SIGQUIT to the script, which is now adequately handled by Ctrl+C – hmedia1 Nov 21 '16 at 02:43
  • Awesome! The first version mysteriously worked.. Bash is a secretive beast. – James M. Lay Nov 21 '16 at 03:10
2

The shell's trap command can be used to ignore ^C temporarily.

trap "" signalname will ignore a signal, and trap - signalname will restore signal behavior to the default.

Here, we ignore SIGINT in the shell, then create a subshell in which SIGINT is restored to its normal behavior and ssh is run.

In the subshell, ssh will respond to ^C, but the parent shell is still ignoring SIGINT and won't be affected if the user types ^C while ssh is running.

echo "FETCHING DB NAMES... (^C to skip)"
trap "" SIGINT                       # ignore ^C
# restore default ^C behavior in subshell,
# then run ssh to output space-delim db list
(trap - SIGINT; ssh "$user@$SERV_A" "$remote_cmd")
trap - SIGINT                        # restore default ^C behavior
echo
printf "Which db do you want to import? >"
read db_name
#rest of script
Mark Plotnick
  • 9,598
  • 1
  • 24
  • 40
  • `trap ""` is a handy trick i'd forgotten about. +1 and have implemented this in my answer to avoid double SIGQUIT's – hmedia1 Nov 21 '16 at 02:47