You can do this entirely with bash 4.3
and above:
_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; r=$?; kill -9 `jobs -p`; exit $r; ) }
Example: _timeout 5 longrunning_command args
Example: { _timeout 5 producer || echo KABOOM $?; } | consumer
Example: producer | { _timeout 5 consumer1; consumer2; }
Example: { while date; do sleep .3; done; } | _timeout 5 cat | less
Needs Bash 4.3 for wait -n
Gives 137 if the command was killed, else the return value of the command.
Works for pipes. (You do not need to go foreground here!)
Works with internal shell commands or functions, too.
Runs in a subshell, so no variable export into the current shell, sorry.
If you do not need the return code, this can be made even simpler:
_timeout() { ( set +b; sleep "$1" & "${@:2}" & wait -n; kill -9 `jobs -p`; ) }
Notes:
Strictly speaking you do not need the ;
in ; )
, however it makes thing more consistent to the ; }
-case. And the set +b
probably can be left away, too, but better safe than sorry.
Except for --forground
(probably) you can implement all variants timeout
supports. --preserve-status
is a bit difficult, though. This is left as an exercise for the reader ;)
This recipe can be used "naturally" in the shell (as natural as for flock fd
):
(
set +b
sleep 20 &
{
YOUR SHELL CODE HERE
} &
wait -n
kill `jobs -p`
)
However, as explained above, you cannot re-export environment variables into the enclosing shell this way naturally.
Edit:
Real world example: Time out __git_ps1
in case it takes too long (for things like slow SSHFS-Links):
eval "__orig$(declare -f __git_ps1)" && __git_ps1() { ( git() { _timeout 0.3 /usr/bin/git "$@"; }; _timeout 0.3 __orig__git_ps1 "$@"; ) }
Edit2: Bugfix. I noticed that exit 137
is not needed and makes _timeout
unreliable at the same time.
Edit3: git
is a die-hard, so it needs a double-trick to work satisfyingly.
Edit4: Forgot a _
in the first _timeout
for the real world GIT example.
Update 2023-08-06: I found a better way to restrict the runtime of git
, so the above is just an example.
The following is no more bash-only as it needs setsid
. But I found no way to reliably create process group leaders with just bash
idioms, sorry.
This recipe is a bit more difficult to use, but very effective, as it not only kills the child, it also kills everything the child places in the same process group.
I now use following:
__git_ps1() { setsid -w /bin/bash -c 'sleep 1 & . /usr/lib/git-core/git-sh-prompt && __git_ps1 "$@" & wait -n; p=$(/usr/bin/ps --no-headers -opgrp $$) && [ $$ = ${p:-x} ] && /usr/bin/kill -9 0; echo "PGRP mismatch $$ $p" >&2' bash "$@"; }
What it does:
setsid -w /bin/bash -c 'SCRIPT' bash "$@"
runs SCRIPT
in a new process group
sleep 1 &
sets the timeout
. /usr/lib/git-core/git-sh-prompt && __git_ps1 "$@" &
runs the git prompt in parallel
/usr/lib/git-core/git-sh-prompt
is for Ubuntu 22.04, change it if needed
wait -n;
waits for either the sleep
or __git_ps1
to return
p=$(/usr/bin/ps --no-headers -opgrp $$) && [ $$ = ${p:-x} ] &&
is just a safeguard to check setsid
worked and we are really a process group leader
$$
works here correctly, as we are within single quotes
kill -9 0
unconditionally kills the entire process group
- all
git
that may still execute
- including the
/bin/bash
echo "PGRP mismatch $$ $p" >&2'
is never reached
- This informs you that either
setsid
is a fake
- or something else (
kill
?) did not work as expected
The safeguard protects against the case that setsid
does not work as advertised. Without your current shell might get killed, which would make it impossible to spawn an interactive shell.
If you use the recipe and trust setsid
, you probably do not need the safeguard, so setsid
is the only non-bash-idiom this needs.