Timing out in a shell script
What about this:
foo=`{ { cat 1>&3; kill 0; } | { sleep 2; kill 0; } } 3>&1`
That is: run the output-producing command and sleep
in the same process group, a process group just for them. Whichever command returns first kills the whole process group.
Would anyone wonder: Yes, the pipe is not used; it's bypassed using the redirections. The sole purpose of it is to have the shell run the two process in the same process group.
As Gilles pointed out in his comment, this won't work in a shell script because the script process would be killed along with the two subprocesses.
One way¹ to force a command to run in a separate process group is to start a new interactive shell:
#!/bin/sh
foo=`sh -ic '{ cat 1>&3; kill 0; } | { sleep 2; kill 0; }' 3>&1 2>/dev/null`
[ -n "$foo" ] && echo got: "$foo" || echo timeouted
But there might be caveats with this (e.g. when stdin is not a tty?). The stderr redirection is there to get rid of the "Terminated" message when the interactive shell is killed.
Tested with zsh
,bash
and dash
. But what about oldies?
B98 suggests the following change, working on Mac OS X, with GNU bash 3.2.57, or Linux with dash:
foo=`sh -ic 'exec 3>&1 2>/dev/null; { cat 1>&3; kill 0; } | { sleep 2; kill 0; }'`
–
1. other than setsid
which appears to be non-standard.
me=$$
(sleep 2; kill $me >/dev/null 2>&1) & nuker=$!
# do whatever
kill $nuker >/dev/null 2>&1
You're already trapping 15 (the numeric version of SIGTERM
, which is what kill
sends unless told otherwise), so you should already be good to go. That said, if you are looking at pre-POSIX, be aware that shell functions may not exist either (they came from System V's shell).
Although coretuils as of version 7.0 includes a timeout command you've mentioned some environments that won't have it. Fortunately pixelbeat.org has a timeout script written sh
.
I've used it before on several occasions and it works very well.
http://www.pixelbeat.org/scripts/timeout (Note: The script below has been slightly modified from the one on pixelbeat.org, see the comments below this answer.)
#!/bin/sh
# Execute a command with a timeout
# Author:
# http://www.pixelbeat.org/
# Notes:
# Note there is a timeout command packaged with coreutils since v7.0
# If the timeout occurs the exit status is 124.
# There is an asynchronous (and buggy) equivalent of this
# script packaged with bash (under /usr/share/doc/ in my distro),
# which I only noticed after writing this.
# I noticed later again that there is a C equivalent of this packaged
# with satan by Wietse Venema, and copied to forensics by Dan Farmer.
# Changes:
# V1.0, Nov 3 2006, Initial release
# V1.1, Nov 20 2007, Brad Greenlee <[email protected]>
# Make more portable by using the 'CHLD'
# signal spec rather than 17.
# V1.3, Oct 29 2009, Ján Sáreník <[email protected]>
# Even though this runs under dash,ksh etc.
# it doesn't actually timeout. So enforce bash for now.
# Also change exit on timeout from 128 to 124
# to match coreutils.
# V2.0, Oct 30 2009, Ján Sáreník <[email protected]>
# Rewritten to cover compatibility with other
# Bourne shell implementations (pdksh, dash)
if [ "$#" -lt "2" ]; then
echo "Usage: `basename $0` timeout_in_seconds command" >&2
echo "Example: `basename $0` 2 sleep 3 || echo timeout" >&2
exit 1
fi
cleanup()
{
trap - ALRM #reset handler to default
kill -ALRM $a 2>/dev/null #stop timer subshell if running
kill $! 2>/dev/null && #kill last job
exit 124 #exit with 124 if it was running
}
watchit()
{
trap "cleanup" ALRM
sleep $1& wait
kill -ALRM $$
}
watchit $1& a=$! #start the timeout
shift #first param was timeout for sleep
trap "cleanup" ALRM INT #cleanup after timeout
"$@" < /dev/tty & wait $!; RET=$? #start the job wait for it and save its return value
kill -ALRM $a #send ALRM signal to watchit
wait $a #wait for watchit to finish cleanup
exit $RET #return the value