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}<> <(:); } 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. |
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()).
12 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.
wow this is black magic, thanks man
2 Trackbacks