No subprocess, no sleep command, no coproc, no nothing? Yes.
Sleeping in bash script is traditionally done with the sleep (1) command, which is external to bash, the command /bin/sleep. However, if you have a bunch of scripts running that all sleep this way, the output of ps looks like a mess, pstree looks like a bigger mess, and every OCD sensor in my brain goes off.
Sample output of pstree:
$ sudo pstree -ps 2828 systemd(1)───urxvt(2826)───bash(2828)───bash(14252)───sleep(14255)
Here, my terminal (urxvt) runs a shell (bash, 2828), that runs a test script (bash, 14252), that runs sleep (14255).
Several bad ideas
This post on Stack Exchange contains plenty of horrible proposed solutions, but does also point out that several distributions of Linux ship a package with loadable bash modules. Among them is an internal sleep command. I didn’t want to rely on that, however.
Stack Overflow has a post on how to sleep forever. Again there are several horrendous ideas, but the answer by Tino is rather clever:
bash -c 'coproc { exec >&-; read; }; eval exec "${COPROC[0]}<&-"; wait'
coproc is a relatively new feature, however, and it uses eval, which, as wooledge.org points out, is a common misspelling of “evil”. We can do better.
Finally asleep
Waiting to read a file descriptor that will never output anything is a clever solution, but we can achieve that without using coproc. instead opting for good old fashioned process substitution.
So I wrote the following function:
snore() { local IFS [[ -n "${_snore_fd:-}" ]] || { exec {_snore_fd}<> <(:) && read -r -t 0 -u $_snore_fd; } 2>/dev/null || {
# workaround for MacOS and similar systems local fifo
fifo=$(mktemp -u) mkfifo -m 700 "$fifo" exec {_snore_fd}<>"$fifo" rm "$fifo" } read ${1:+-t "$1"} -u $_snore_fd || : }
So what does that do? Well, this:
local IFS | Reset IFS in case it’s set to something weird. |
[[ -n “${_snore_fd:-}” ]] | Checks if the $_snore_fd variable has already been declared. If so, we are good to go. The :- is there to subtitute with an empty string in case you’re using “set -eu”, which would exit with an error if the variable wasn’t set already. |
exec {_snore_fd}<> | Assigns the next available file descriptor to the “_snore_fd” variable. “_snore_fd” will be a number signifying the assigned file descriptor after this. |
<(:) | Process substitution: reading from a subshell that simply runs “:”, or “true” if you will, and then exits |
read | Attempts to read input, though it won’t get any |
${1:+-t “$1”} | Parameter expansion: If the snore() function was provided a parameter, it will pass it along to read as an argument for -t (timeout). If no parameters were provided, -t will not be specified, and read will hang forever. |
-u $_snore_fd | Specifies that read should use the value of $_snore_fd as its input file descriptor |
|| : | Making sure read returns 0, for coding with -e set. This will run : if read fails, and : always returns 0. |
Let’s test it!
Here’s a short script to compare the efficiency of snore() to that of /bin/sleep. It runs each operation 1000 times, for a total of what should be 10 seconds for each.
#!/usr/bin/env bash set -u set -e snore() { local IFS [[ -n "${_snore_fd:-}" ]] || exec {_snore_fd}<> <(:) read ${1:+-t "$1"} -u $_snore_fd || : } time for ((i=0; i<1000; i++)); do snore 0.01; done time for ((i=0; i<1000; i++)); do sleep 0.01; done
The snore() function runs faster than /bin/sleep, at least on my system. That’s not to say it sleeps too quickly – one second is still one second – but if called in quick succession, one can see that the snoring loop is faster than the sleeping one:
$ /tmp/test </dev/null real 0m10.226s user 0m0.144s sys 0m0.036s real 0m11.674s user 0m0.060s sys 0m0.232s
As you can see, calling snore() 1000 times has a combined overhead of 0.226 seconds, while /bin/sleep measured 1.674 seconds. This is of course utterly insignificant in real world applications, but it’s interesting none the less.
No more sleep processes
Aside from the completely insignificant performance differences, my OCD was satisfied, as a script running snore() has no child process to wait for, and the subshell we spawn (once) disappears immediately. Here’s pstree while I run a script that snores:
$ sudo pstree -ps 2828 systemd(1)───urxvt(2826)───bash(2828)───bash(19247)
So my terminal runs a shell, and that shell runs the script, but there’s no sleep call, and no other subprocess. There’s simply the interactive shell waiting for the script. Excellent.
As an added bonus, there will no longer be any of the usual issues of various sleep processes hanging around after killing processes, or preventing them from being killed in the first place.
Halt and Catch Fire
Going back to the question on stack overflow, you may have noticed the parameter processing of snore() allowing for no parameters to be passed. This means that if you don’t pass any parameters to snore(), -t (timeout) will not be specified for the call to read, and read will hang forever. I don’t know why you’d want this, but now you can.
Update (June 12, 2019)
Added a workaround for MacOS and similar systems, using a short-lived FIFO to read from (only created on the first call to snore()).
Update (July 23, 2023)
Added a workaround for Bash 5.2 (thank you, anonymous commenter, for letting me know about the issue). Bash 5.2 would let the subprocess spawned by <(:) linger until the first read call completed. By using read -r -t 0 on it initially, this subprocess should go away immediately and not clutter up process lists, as intended.
21 Comments
Really cool. Thanks for posting this.
Just tried a test script with only the snore function and a ‘snore 2’ call to it, but got a syntax error: unexpected redirection error on the starting with [[.
sh will say that. This is a bash function. Make sure you run it with bash.
read -t 3 < >(:)
That is exactly what’s being done here, except if you don’t save the file descriptor, you’re creating a new subprocess each time.
Very cool!
Just what I need for my collectd exec plugin script.
It does not work in MacOS.
It shows “/dev/fd/62: Permission denied” when execute `exec {_snore_fd} <(:)`.
My bash version: 4.4.12(1)-release (x86_64-apple-darwin15.6.0)
It is indeed not fully portable.I unfortunately don’t have a MacOS system available to develop a workaround.
If you figure something out, please let me know.
I got access to a MacOS machine and added a quick workaround for it. It’s not pretty, but it does seem to do the job, on Bash4 and upwards.
That is cool. I have tested it and found that `mktemp -u` may be slow relative to snore 0.1s. It costs 0.012~0.016s. So the precision of snore function may be 0.1s. And `snore 0.01` will be inaccurate.
0.1s precision is good enough to me. Thanks!
Yes, the first call to snore() will have some overhead with that workaround. Every subsequent call in the same script, as long as the _snore_fd variable is preserved, should be as accurate as the other methods, however.
Hi, what is the purpose of :- in ${_snore_fd:-} ?
If i’m not mistaken, this is a parameter substitution and there is supposed to be a word after the dash to be substituted for _snore_fd, but there isn’t anything here.
It’s for people writing scripts with “set -eu”, which would exit with an error if the variable wasn’t set. It makes no difference if you don’t use those settings.
wow this is black magic, thanks man
Noticed lately it creates subprosess of itself.
$ rngbd &
[1] 3207
$ pstree -ps 3207
runit(1)───tabbed(1006)───st(1176)───bash(1177)───rngbd(3207)───rngbd(3209)
it did not use to do this, maybe changes in bash?
Thank you for bringing this to my attention. Bash 5.2 has apparently changed some of what made this hack work. I’ve updated the function to do a zero timeout read when initially creating the file descriptor, which gets rid of the issue.
First, thank you for your work.
I think you misspelled $_sf_sleep_fd and should be $_sleep_fd
Also I suggest read -r ${1:+-t “$1”} -u $_snore_fd || :
I misspelled my comment also :)))
I mean $_sf_sleep_fd should be $_snore_fd
Correct me if I’m wrong.
Hey. Sorry about that. Copy/paste error 😀
The function is called something else internally.
Thank you for the correction.
It’s faster and more accurate ;). Sweet. I don’t think the local IFS is really needed, or how would this ever be needed?
You’re right it likely doesn’t do much in this case. The read command can behave in very weird ways if that variable is off though.
This is brilliant – thank you.
using a modified version of the function that overcomes the bash 5.2 issue, but doesn’t require the Mac workaround :
[[ -n “${_snore_fd:-}” ]] || { exec {_snore_fd} <(:) && read -r -t 0 -u $_snore_fd; }
read ${1:+-t "$1"} -u $_snore_fd || :
time for ((i=0; i<1000; i++)); do snore 0.01; done
The test script runs the 1st loop then exits with strange timings. The curly braces look balanced – so I can't see why.
real 0m0.000s
user 0m0.000s
sys 0m0.000s
I'm running bash v5.1.16
3 Trackbacks