Is there a way to terminate only the first command in a sequence of commands? - bash

When using tmux I use the wait-for feature where the tmux session is getting notified after the finishing of a command. Sometimes however I want to terminate the command sequence without terminating the wait-for part so the original script doesn't hang.
Basically, if I have:
command 1; command 2
hitting Ctrl-C exits both where I mainly want to exit command 1 but continue executing command 2 (which is the wait-for acknowledgment so the script doesn't hang).
Is there a way to do that?
I already tried:
command 1 || command 2
But Ctrl-C still exits both.

You can try running command 1 in the background and trap the signal that Ctrl + C sends.
#! /bin/bash
(command 1) & # Runs c1 in the background
pid=$! # Stores the PID of the subshell
trap "kill -INT $pid" SIGINT # Makes Ctrl+C kill the background process
wait # Waits for completion of c1
trap - SIGINT # Restores the default behaviour of Ctrl+C
command 2 # Runs the second command

To let the command exit but the script continue on Ctrl-C, just set a no-op sigint trap:
trap "true" INT
sleep 30
echo "Continuing"
If you want to restore the behavior of killing the script, you can use trap - INT.

The following should ensure that if you press ctrl-C, command1, plus any child processes that it may have, get the SIGINT.
#!/bin/bash
# Use "set -m" to test if "-m" option is currently set
# If set, this will ensure that any subprocesses are started
# as process group leaders (we'll need this later)
if [ -z "${-//[^m]/}" ] # -m option not already set
then
set -m
setm=1
else
setm=0
fi
# launch the command and capture its pid
command1 &
pid=$!
# install a trap so that if SIGINT is received, then every
# process in the process group of which command1 is leader
# is sent a SIGINT (note the "-" before $pid)
trap "kill -INT -$pid" SIGINT
# wait for command1 to finish (ignoring any other previously launched
# children that finish meanwhile)
wait $pid
# undo "set -m" setting as appropriate
if [ $setm -eq 1 ]
then
set +m
fi
# cancel the trap
trap - SIGINT
# and carry on
command2
For example, if command1 is itself a shell script, then the command that is being run by that shell script should be properly terminated.
A slight side-effect of using the -m option is that if you do press ctrl-C, then you will receive a message such as:
[1]+ Interrupt command1
This may be deferred until after the next command completes. You could insert a short sleep before command2 (e.g. sleep 0.1) so that any such notification is delivered at the end of the sleep (before command2 runs) rather than after command2.

Related

How can I silence the "Terminated" message when my command is killed by timeout?

By referencing bash: silently kill background function process and Timeout a command in bash without unnecessary delay, I wrote my own script to set a timeout for a command, as well as silencing the kill message.
But I still am getting a "Terminated" message when my process gets killed. What's wrong with my code?
#!/bin/bash
silent_kill() {
kill $1 2>/dev/null
wait $1 2>/dev/null
}
timeout() {
limit=$1 #timeout limit
shift
command=$* #command to run
interval=1 #default interval between checks if the process is still alive
delay=1 #default delay between SIGTERM and SIGKILL
(
((t = limit))
while ((t > 0)); do
sleep $interval;
#kill -0 $$ || exit 0
((t -= interval))
done
silent_kill $$
#kill -s SIGTERM $$ && kill -0 $$ || exit 0
sleep $delay
#kill -s SIGKILL $$
) &> /dev/null &
exec $*
}
timeout 1 sleep 10
There's nothing wrong with your code, that "Terminated" message doesn't come from your script but from the invoking shell (the one you launch your script
from).
You can deactivate if by disabling job control:
$ set +m
$ bash <your timeout script>
Perhaps bash has moved on in 4 years. I do know you can avoid
getting Terminated by disowning a child process. You can no longer job control it though. Eg:
$ sleep 100 &
[1] 15436
$ disown -r
$ kill -9 15436
help disown:
disown [-h] [-ar] [jobspec ...]
Remove jobs from current shell.
Removes each JOBSPEC argument from the table of active jobs. Without
any JOBSPECs, the shell uses its notion of the current job.
-a remove all jobs if JOBSPEC is not supplied
-h mark each JOBSPEC so that SIGHUP is not sent to the job if the shell receives a SIGHUP
-r remove only running jobs
Internally the shell maintains a list of children it forked and wait()s for any of them to exit or be killed. When a child's exit status was collected, the shell prints a message. This is called monitoring in shell parlance.
It seems you want to turn off monitoring. Monitoring is managed with the m option; to turn it on, use set -m (the default at startup). To turn it off, set +m.
Note that monitoring off also disables messages for asynchronous jobs, e.g. no more messages like
$ sleep 5 &
[1] 59468
$
[1] + done sleep 5
$

Sending SIGINT to foreground process works but not background

I have two scripts. script1 spawns script2 and then sends a SIGINT signal to it. However the trap in script2 doesn't seem to work?!
script1:
#!/bin/bash
./script2 &
sleep 1
kill -SIGINT $!
sleep 2
script2:
#!/bin/bash
echo "~~ENTRY"
trap 'echo you hit ctrl-c, waking up...' SIGINT
sleep infinity
echo "~~EXIT"
If change ./script2 & to ./script2 and press CTRL+C the whole things works fine. So what am I doing wrong?
You have several issues in your examples, at the end I have a solution for your issue:
your first script seems to miss a wait statement, thus, it exits
after roughly 3 seconds. However script2 will remain in memory and
running.
How do you want bash to automatically figure which process it should
send the SIGINT signal ?
Actually bash will disable SIGINT (and SIGQUIT) on background processes and they can't be enabled (you can check by running trap command alone to check the current status of set traps). See How to send a signal SIGINT from script to script ? BASH
So your script2 is NOT setting a trap on SIGINT because it's a background process, both SIGINT and SIGQUIT are ignored and can't be anymore trapped nor resetted on background processes.
As a reference, here are the documentation from bash related to your issue:
Process group id effect on background process (in Job Control section of doc):
[...] processes whose process group ID is equal to the current terminal
process group ID [..] receive keyboard-generated signals such as
SIGINT. These processes are said to be in the foreground.
Background processes are those whose process group ID differs from
the terminal's; such processes are immune to keyboard-generated
signals.
Default handler for SIGINT and SIGQUIT (in Signals section of doc):
Non-builtin commands run by bash have signal handlers set to the values inherited by the shell from its parent. When job control is not in effect, asynchronous commands ignore SIGINT and SIGQUIT in addition to these inherited handlers.
and about modification of traps (in trap builtin doc):
Signals ignored upon entry to the shell cannot be trapped or reset.
SOLUTION 1
modify your script1 to be:
#!/bin/bash
{ ./script2; } &
sleep 1
subshell_pid=$!
pid=$(ps -ax -o ppid,pid --no-headers | sed -r 's/^ +//g;s/ +/ /g' |
grep "^$subshell_pid " | cut -f 2 -d " ")
kill -SIGINT $pid
sleep 2
wait ## Don't forget this.
How does this work ? Actually, the usage of { and } will create a subshell, that will be limited by the explained limitation on SIGINT, because this subshell is a background process. However, the subshell's own subprocess are foreground and NOT background processes (for our subshell scope)... as a consequence, they can trap or reset SIGINT and SIGQUIT signals.
The trick is then to find the pid of this subprocess in the subshell, here I use ps to find the only process having the subshell's pid as parent pid.
SOLUTION 2
Actually, only direct new process managed as job will get their SIGINT and SIGQUIT ignored. A simple bash function won't. So if script2 code was in a function sourced in script1, here would be your new script1 that doesn't need anything else:
#!/bin/bash
script2() {
## script2 code
echo "~~ENTRY"
trap 'echo you hit ctrl-c, waking up...' SIGINT
sleep infinity
echo "~~EXIT"
}
## script1 code
script2 &
sleep 1
kill -SIGINT $!
sleep 2
This will work also. Behind the scene, the same mecanism than SOLUTION 1 is working: a bash function is very close to the { } construct.
I guess what you are trying to achieve is that when script2 receives the SIGINT it continues and prints the message. Then, you need
#!/bin/bash
echo "~~ENTRY"
trap 'echo you hit ctrl-c, waking up...; CONT=true' SIGINT
CONT=false
while ! $CONT
do
sleep 1
done
echo "~~EXIT"

Catch SIGINT in bash, handle AND ignore

Is it possible in bash to intercept a SIGINT, do something, and then ignore it (keep bash running).
I know that I can ignore the SIGINT with
trap '' SIGINT
And I can also do something on the sigint with
trap handler SIGINT
But that will still stop the script after the handler executes. E.g.
#!/bin/bash
handler()
{
kill -s SIGINT $PID
}
program &
PID=$!
trap handler SIGINT
wait $PID
#do some other cleanup with results from program
When I press ctrl+c, the SIGINT to program will be sent, but bash will skip the wait BEFORE program was properly shut down and created its output in its signal handler.
Using #suspectus answer I can change the wait $PID to:
while kill -0 $PID > /dev/null 2>&1
do
wait $PID
done
This actually works for me I am just not 100% sure if this is 'clean' or a 'dirty workaround'.
trap will return from the handler, but after the command called when the handler was invoked.
So the solution is a little clumsy but I think it does what is required. trap handler INT also will work.
trap 'echo "Be patient"' INT
for ((n=20; n; n--))
do
sleep 1
done
The short answer:
SIGINT in bash can be caught, handled and then ignored, assumed that "ignored" here means that bash continues to run the script.
The wanted actions of the handler can even be postponed to build a kind of "transaction" so that SIGINT will be fired (or "ignored") AFTER a group of statements have done their work.
But since the above example touches many aspects of bash (foreground vs. background behavior, trap and wait) AND 8 years went away since then, the solution discussed here may not immediately work on all systems without further finetuning.
The solution discussed here was successfully tested on a "Linux mint-mate 5.4.0-73-generic x86_64" system with "GNU bash, Version 4.4.20(1)-release":
The wait shell builtin command IS DESIGNED to be interruptable. But one can examine the exit status of wait, which is 128 + signal number = 130 (in the case of SIGINT).
So if you want to trick around and wait til the background is process really finished, one can also do something like this:
wait ${programPID}
while [ $? -ge 128 ]; do
# 1st opportunity to place your **handler actions** is here
wait ${programPID}
done
But let it also said that we ran into a bug/feature while testing all of this. The problem was that wait kept on returning 130 even after the process in the background was no longer there. The documentation says that wait will return 127 in the case of a false process id, but this did not happen in our tests.
Keep in mind to check the existence of the background process before running the wait command in the while loop, if you also run into this problem.
Assumed that the following script is your program, which simply counts down from 5 to 0 and also tee's its output to a file named program.out. The while loop here is considered as a "transaction", which shall not be disturbed by SIGINT. And one last comment: This code does NOT ignore SIGINT after doing postponed actions, but instead restores the old SIGINT handler and raises a SIGINT:
#!/bin/bash
rm -f program.out
# Will be set to 1 by the SIGINT ignoring/postponing handler
declare -ig SIGINT_RECEIVED=0
# On <CTRL>+C or "kill -s SIGINT $$" set flag for [later|postponed] examination
function _set_SIGINT_RECEIVED {
SIGINT_RECEIVED=1
}
# Remember current SIGINT handler
old_SIGINT_handler=$(trap -p SIGINT)
# Prepare for later restoration via ${old_SIGINT_handler}
old_SIGINT_handler=${old_SIGINT_handler:-trap - SIGINT}
# Start your "transaction", which should NOT be disturbed by SIGINT
trap -- '_set_SIGINT_RECEIVED' SIGINT
count=5
echo $count | tee -a program.out
while (( count-- )); do
sleep 1
echo $count | tee -a program.out
done
# End of your "transaction"
# Look whether SIGINT was received
if [ ${SIGINT_RECEIVED} -eq 1 ]; then
# Your **handler actions** are here
echo "SIGINT was received during transaction..." | tee -a program.out
echo "... doing postponed work now..." | tee -a program.out
echo "... restoring old SIGINT handler and sending SIGINT" | tee -a program.out
echo "program finished after SIGINT postponed." | tee -a program.out
${old_SIGINT_handler}
kill -s SIGINT $$
fi
echo "program finished without having received SIGINT." | tee -a program.out
But let it also be said here that we ran into problems after sending program in the background. The problem was that program inherited a trap '' SIGINT which means that SIGINT was generally ignored and program was NOT able to set another handler via trap -- '_set_SIGINT_RECEIVED' SIGINT.
We solved this problem by putting program in a subshell and sending this subshell in the background, as you will see now in the MAIN script example, which runs in the foreground. And one last comment also: In this script you can decide via variable ignore_SIGINT_after_handling whether to finally ignore SIGINT and continue to run the script OR to execute the default SIGINT behavior after your handler action has finished its work:
#!/bin/bash
# Will be set to 1 by the SIGINT ignoring/postponing handler
declare -ig SIGINT_RECEIVED=0
# On <CTRL>+C or "kill -s SIGINT $$" set flag for later examination
function _set_SIGINT_RECEIVED {
SIGINT_RECEIVED=1
}
# Set to 1 if you want to keep bash running after handling SIGINT in a particular way
# or to 0 (or any other value) to run original SIGINT action after postponing SIGINT
ignore_SIGINT_after_handling=1
# Remember current SIGINT handler
old_SIGINT_handler=$(trap -p SIGINT)
# Prepare for later restoration via ${old_SIGINT_handler}
old_SIGINT_handler=${old_SIGINT_handler:-trap - SIGINT}
# Start your "transaction", which should NOT be disturbed by SIGINT
trap -- '_set_SIGINT_RECEIVED' SIGINT
# Do your work, for eample
(./program) &
programPID=$!
wait ${programPID}
while [ $? -ge 128 ]; do
# 1st opportunity to place a part of your **handler actions** is here
# i.e. send SIGINT to ${programPID} and make sure that it is only sent once
# even if MAIN receives more SIGINT's during this loop
wait ${programPID}
done
# End of your "transaction"
# Look whether SIGINT was received
if [ ${SIGINT_RECEIVED} -eq 1 ]; then
# Your postponed **handler actions** are here
echo -e "\nMAIN is doing postponed work now..."
if [ ${ignore_SIGINT_after_handling} -eq 1 ]; then
echo "... and continuing with normal program execution..."
else
echo "... and restoring old SIGINT handler and sending SIGINT via 'kill -s SIGINT \$\$'"
${old_SIGINT_handler}
kill -s SIGINT $$
fi
fi
# Restore "old" SIGINT behaviour
${old_SIGINT_handler}
# Prepare for next "transaction"
SIGINT_RECEIVED=0
echo ""
echo "This message has to be shown in the case of normal program execution"
echo "as well as after a caught and handled and then ignored SIGINT"
echo "End of MAIN script received"
Hope this helps a bit.
Shall everybody have a good time.
i had the same problem: my script was exiting after my sigint handler
i solved this by recursion
#! /bin/sh
# devloop.sh
# run command in infinite loop
# wait before restarting, to allow stopping the loop
# license: MIT, author: milahu
# https://stackoverflow.com/questions/15785522/catch-sigint-in-bash-handle-and-ignore
restart_delay=2
command="$1" # TODO use all args: $#
# example: drop cache, run vite
#command="rm -rf node_modules/.vite/ ; npx vite --clearScreen false"
if [ -z "$command" ]
then
command="( set -x; sleep 5 ); false # example command: sleep 5 seconds, set rc=1"
fi
loop_next() {
echo
echo "starting command. hit Ctrl+C to restart"
echo " $command"
(eval "$command") &
command_pid=$!
#echo "main pid: $$"; echo "cmd pid: $command_pid" # debug
restart_command() {
echo
echo "restarting command in $restart_delay seconds. hit Ctrl+C to stop"
sleep $restart_delay
loop_next # recursion
}
stop_command() {
echo
echo "got Ctrl+C -> stopping command"
kill $command_pid
trap exit SIGINT # handle second Ctrl+C
restart_command
}
trap stop_command SIGINT # handle first Ctrl+C
wait $command_pid # this is blocking
echo "command stopped. return code: $?"
restart_command
}
echo starting loop
loop_next

Set trap in bash for different process with PID known

I need to set a trap for a bash process I'm starting in the background. The background process may run very long and has its PID saved in a specific file.
Now I need to set a trap for that process, so if it terminates, the PID file will be deleted.
Is there a way I can do that?
EDIT #1
It looks like I was not precise enough with my description of the problem. I have full control over all the code, but the long running background process I have is this:
cat /dev/random >> myfile&
When I now add the trap at the beginning of the script this statement is in, $$ will be the PID of that bigger script not of this small background process I am starting here.
So how can I set traps for that background process specifically?
(./jobsworthy& echo $! > $pidfile; wait; rm -f $pidfile)&
disown
Add this to the beginning of your Bash script.
#!/bin/bash
trap 'rm "$pidfile"; exit' EXIT SIGQUIT SIGINT SIGSTOP SIGTERM ERR
pidfile=$(tempfile -p foo -s $$)
echo $$ > "$pidfile"
# from here, do your long running process
You can run your long running background process in an explicit subshell, as already shown by Petesh's answer, and set a trap inside this specific subshell to handle the exiting of your long running background process. The parent shell remains unaffected by this subshell trap.
(
trap '
trap - EXIT ERR
kill -0 ${!} 1>/dev/null 2>&1 && kill ${!}
rm -f pidfile.pid
exit
' EXIT QUIT INT STOP TERM ERR
# simulate background process
sleep 15 &
echo ${!} > pidfile.pid
wait
) &
disown
# remove background process by hand
# kill -TERM ${!}
You do not need trap to just run some command after a background process terminates, you can instead run through a shell command line and add the command following after the background process, separated with semicolon (and let this shell run in the background instead of the background process).
If you still would like to have some notification in your shell script send and trap SIGUSR2 for instance:
#!/bin/sh
BACKGROUND_PROCESS=xterm # for my testing, replace with what you have
sh -c "$BACKGROUND_PROCESS; rm -f the_pid_file; kill -USR2 $$" &
trap "echo $BACKGROUND_PROCESS ended" USR2
while sleep 1
do
echo -n .
done

bash trap will echo from keyboard Ctrl-C while not kill 2

Say I have a script:
#!/bin/bash
# test_trap.sh
trap "echo SIGINT captured!" SIGINT
echo $$
sleep 1000
I know trap COMMAND will only be executed after sleep 1000 finishes when it receives SIGINT signal. But the command of trap will be executed when I pressed keyboard Ctrl-C:
> sh test_sh.sh
50138
^CSIGINT captured!
And using kill -s SIGINT will not.
What am I missing here?
The bash version is GNU bash, 4.2.46(2)-release
With kill -s SIGINT 50138, you are only sending the signal to the shell's process, and that has to wait for sleep 1000 to finish, because sleep doesn't receive the signal.
Control-C, though, causes the terminal to send SIGINT to every process in the current process group, so both your shell script and sleep receive it. Your script still doesn't process the trap command until sleep completes, but sleep exits immediately in response to the SIGINT it just received from the terminal.
If your kill supports it, you can also use kill -s SIGINT -50138 (note the negative process id) to send SIGINT to the entire process group.

Resources