Why "cat /dev/urandom" hung my bash script?
You should never use cat
with /dev/urandom
. Nor should you use any utilities which are designed for text files.
/dev/urandom
is a continuous stream of random data. It will never produce an end of file. Buffered reads will fill the read buffer, so even if you are piping the output of cat
into some other program, the read won't be terminated until the pipe is closed.
None of that would be anything other than inefficient, except that when you read /dev/urandom
, you are using up entropy (randomness), which is a precious resource. Once entropy is used up, /dev/urandom
's output will be less random, which defeats the purpose. (More entropy will be collected, but it takes a while to build up.)
All of this goes double for /dev/random
, because when it runs out of entropy, it usually blocks. (Except on OSs which make /dev/random
a synonym for /dev/urandom
.)
Consequently, you should always read exactly the amount of random data you need, and no more.
Apparently, you're aiming at 24 alphanumeric characters. There are 62 possible alphanumeric characters; it would simplify things greatly if you were willing to allow two other characters to bring the total to 64. In that case, you could produce 24 characters by extracting 18 bytes of randomness and passing it through a base64 encoder. To extract a precise amount of data, use dd
, which is designed for the purpose:
dd bs=18 count=1 if=/dev/urandom | base64 | tr +/ _.
(The tr
at the end translates the two non-alphanumeric characters produced by base64
into two different characters which are more filename-friendly. Just a suggestion.)
If you're determined to use precisely alphanumeric characters, you could use a rejection strategy similar to the one you're currently using, but based on the above. Unfortunately, it's not possible to predict exactly how much input you'll need in this case, so the simplest approach is to read a little bit extra, and retry in the rare case that you don't get enough:
# Here we produce 28 characters each time
until s=$(dd bs=21 count=1 if=/dev/urandom |
LC_ALL=C tr -cd A-Za-z0-9)
((${#s} >= 24)); do :; done
# When the loop ends we have at least 24 characters; truncate
s=${s:0:24}
If you don't have bash, you can replace ((${#s} >= 24))
with [ ${#s} -ge 24 ]
and s=${s:0:24}
with s=$(printf %.24s $s)
But if you're just trying to generate good random filenames, you should use mktemp
, which allows you to specify a skeleton for the names, and also verifies that the generated name is not already present. See man mktemp
.
Actually cat /dev/urandom
never ends on its own. But when head -1
reads first line, it exits, thus closing stdin and closing a pipe. OS raises SIGPIPE
to fold
is also exits, and so on, so cat /dev/urandom
ends eventually.
In your case, something blocking SIGPIPE
, i.e. trap can do that:
$ trap '' PIPE
$ cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 24 | head -n 1
7FazO6mnsIow3ylkvEHB55jE
(hungs)
Try to re-enable it in subshell:
( trap - PIPE ; cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 24 | head -n 1 )