Differences between revisions 34 and 35
Revision 34 as of 2012-11-27 14:25:08
Size: 5465
Editor: geirha
Comment: move expansion outside printf's format string
Revision 35 as of 2012-12-21 01:17:21
Size: 10440
Editor: GreyCat
Comment: Add section on external random data sources; clean up the rest.
Deletions are marked like this. Additions are marked like this.
Line 2: Line 2:
== How can I randomize (shuffle) the order of lines in a file? (Or select a random line from a file, or select a random file from a directory.) == == How can I randomize (shuffle) the order of lines in a file? Or select a random line from a file, or select a random file from a directory? ==
Line 6: Line 6:
    #bash
    randomize() {
     while IFS='' read -r l ; do printf '%d\t%s\n' "$RANDOM" "$l"; done |
        sort -n |
        cut -f2-
    }
# Bash/Ksh
randomize() {
    while IFS='' read -r l ; do printf '%d\t%s\n' "$RANDOM" "$l"; done |
    sort -n |
    cut -f2-
}
Line 14: Line 14:
RANDOM is supported by [[BASH]], KornShell but is not defined by posix. RANDOM is supported by [[BASH]] and KornShell, but is not defined by POSIX.
Line 17: Line 17:
Line 18: Line 19:
    # Bourne
    awk '
     BEGIN { srand() }
     { print rand() "\t" $0 }
    ' |
    sort -n | # Sort numerically on first (random number) column
    cut -f2- # Remove sorting column
# Bourne
awk '
    BEGIN { srand() }
    { print rand() "\t" $0 }
' |
sort -n | # Sort numerically on first (random number) column
cut -f2- # Remove sorting column
Line 27: Line 28:
This is (possibly) faster than the previous solution, but will not work for very old [[AWK]] implementations (try "nawk", or "gawk", or /usr/xpg4/bin/awk if available). (Note that awk use the epoch time as a seed for srand(), which might not be random enough for you) This is (possibly) faster than the previous solution, but will not work for very old [[AWK]] implementations (try `nawk`, or `gawk`, or `/usr/xpg4/bin/awk` if available). (Note that AWK uses the epoch time as a seed for `srand()`, which may or may not be random enough for you.)

Other non-portable utilities that can shuffle/randomize a file:
 * GNU `shuf` (in recent enough GNU coreutils)
 * GNU `sort -R` (coreutils 6.9)

Recent GNU `sort` has the `-R` (aka `--random-sort`) flag. Oddly enough, it only works for the generic [[locale]]:
{{{
LC_ALL=C sort -R file # output the lines in file in random order
LC_ALL=POSIX sort -R file # output the lines in file in random order
LC_ALL=en_US sort -R file # effectively ignores the -R option
}}}

For more details, see `info coreutils sort` or an equivalent manual.

=== Shuffling an array ===
Line 32: Line 48:
    # Uses a global array variable. Must be compact (not a sparse array).
    # Bash syntax.
    shuffle() {
     local i tmp size max rand
# Uses a global array variable. Must be compact (not a sparse array).
# Bash syntax.
shuffle() {
   local i tmp size max rand
Line 37: Line 53:
     # $RANDOM % (i+1) is biased because of the limited range of $RANDOM
     # Compensate by using a range which is a multiple of the array size.
       size=${#array[*]}
     max=$(( 32768 / size * size ))
   # $RANDOM % (i+1) is biased because of the limited range of $RANDOM
   # Compensate by using a range which is a multiple of the array size.
   size=${#array[*]}
   max=$(( 32768 / size * size ))
Line 42: Line 58:
     for ((i=size-1; i>0; i--)); do
     while (( (rand=$RANDOM) >= max )); do :; done
          rand=$(( rand % (i+1) ))
     tmp=${array[i]} array[i]=${array[rand]} array[rand]=$tmp
       done
    }
   for ((i=size-1; i>0; i--)); do
      while (( (rand=$RANDOM) >= max )); do :; done
      rand=$(( rand % (i+1) ))
      tmp=${array[i]} array[i]=${array[rand]} array[rand]=$tmp
   done
}
Line 52: Line 68:
If we just want the unbiased random number picking function, we can split that out separately:

{{{
# Returns random number from 0 to ($1-1) in global var 'r'.
# Bash syntax.
rand() {
    local max=$((32768 / $1 * $1))
    while (( (r=$RANDOM) >= max )); do :; done
    r=$(( r % $1 ))
}
}}}

This `rand` function is better than using `$((RANDOM % n))`. For simplicity, many of the remaining examples on this page may use the modulus approach. In all such cases, switching to the use of the `rand` function will give better results; this improvement is left as an exercise for the reader.

=== Selecting a random line/file ===
Line 54: Line 86:
{{{
# Bash
n=$(wc -l <"$file") # Count number of lines.
r=$((RANDOM % n + 1)) # Random number from 1..n (see warnings above!)
sed -n "$r{p;q;}" "$file" # Print the r'th line.

# POSIX with (new) AWK
awk -v n="$(wc -l <"$file")" \
  'BEGIN{srand();l=int((rand()*n)+1)} NR==l{print;exit}' "$file"
}}}

(See [[BashFAQ/011|FAQ 11]] for more info about printing the r'th line.)

The next example sucks the entire file into memory. This approach saves time rereading the file, but obviously uses more memory. (Arguably: on systems with sufficient memory and an effective disk cache, you've read the file into memory by the earlier methods, unless there's insufficient memory to do so, in which case you shouldn't, QED.)
Line 56: Line 102:
   # Bash
   n=$(wc -l < "$file") # Count number of lines.
   r=$((RANDOM % n + 1)) # Random number from 1..n. (See below)
   sed -n "$r{p;q;}" "$file" # Print the r'th line.

   #posix with awk
   awk -v n="$(wc -l<"$file")" 'BEGIN{srand();l=int((rand()*n)+1)} NR==l{print;exit}' "$file"
}}}

(see [[BashFAQ/011|this faq]] for more info about printing the n'th line.)

The next example sucks the entire file into memory. This approach saves time reopening the file, but obviously uses more memory. (Arguably: on systems with sufficient memory and an effective disk cache, you've read the file into memory by the earlier methods, unless there's insufficient memory to do so, in which case you shouldn't, QED.)

{{{
   # Bash
   unset lines i
   while IFS= read -r 'lines[i++]'; do :; done < "$file" # See FAQ 5
   n=${#lines[@]}
   r=$((RANDOM % n)) # see below
   echo "${lines[r]}"
# Bash
unset lines n
while IFS= read -r 'lines[n++]'; do :; done < "$file" # See FAQ 5
r=$((RANDOM % n)) # See warnings above!
echo "${lines[r]}"
Line 83: Line 114:
    # Bash
    files=(*.ogg) # Or *.gif, or *
    n=${#files[@]} # For aesthetics
    xmms -- "${files[RANDOM % n]}" # Choose a random element
# Bash
files=(*.ogg) # Or *.gif, or *
n=${#files[@]} # For readability
xmms -- "${files[RANDOM % n]}" # Choose a random element
Line 89: Line 120:
Note that these last few examples use a simple modulus of the RANDOM variable, so the results are biased. If this is a problem for your application, then use the anti-biasing technique from the Knuth-Fisher-Yates example, above. === Known bugs ===
 . --([[http://lists.gnu.org/archive/html/bug-bash/2010-01/msg00042.html]] points out a surprising pitfall concerning the use of `RANDOM` without a leading `$` in certain mathematical contexts. (Upshot: you should prefer `n=$((...math...)); ((array[n]++))` over `((array[...math...]++))` in almost every case.))--
  . Behavior described appears reversed in current versions of mksh, ksh93, Bash, and Zsh. Still something to keep in mind for legacy. -ormaaj
Line 91: Line 124:
Other non portable utilities:
 * GNU Coreutils {{{shuf}}} (in recent enough coreutils)
 * GNU sort -R
=== Using external random data sources ===
Line 95: Line 126:
Speaking of GNU coreutils, as of version 6.9 GNU sort has the -R (aka --random-sort) flag. Oddly enough, it only works for the generic locale:
{{{
     LC_ALL=C sort -R file # output the lines in file in random order
     LC_ALL=POSIX sort -R file # output the lines in file in random order
     LC_ALL=en_US sort -R file # effectively ignores the -R option
Some people feel the shell's builtin `RANDOM` parameter is not sufficiently random for their applications. Typically this will be an interface to the C library's `rand(3)` function, although the Bash manual does not specify the implementation details. Some people feel their application requires cryptographically stronger random data, which would have to be supplied by some external source.

Before we explore this, we should point out that often people want to do this as a first step in writing some sort of random password generator. If that is your goal, you should at least consider using a password generator that has already been written, such as [[http://sourceforge.net/projects/pwgen/|pwgen]].

Now, if we're considering the use of external random data sources in a Bash script, we face several issues:
 * The data source will probably not be portable. Thus, the script will only be usable in special environments.
 * If we simply grab a byte (or a single group of bytes large enough to span the desired range) from the data source and do a modulus on it, we will run into the bias issue described earlier on this page. There is absolutely no point in using an expensive external data source if we're just going to bias the results with sloppy code! To work around that, we may need to grab bytes (or groups of bytes) repeatedly, until we get one that can be used without bias.
 * Bash can't handle raw bytes very well, so each time we grab a byte (or group) we need to do something to it to turn it into a number that Bash can read. This may be an expensive operation. So, it may be more efficient to grab ''several'' bytes (or groups), and do the conversion to readable numbers, all at once.
 * Depending on the data source, these random bytes may be precious, so grabbing a lot of them all at once and discarding ones we don't use might be more expensive (by whatever metric we're using to measure such costs) than grabbing one byte at a time, even counting the conversion step. This is something you'll have to decide for yourself, taking into account the needs of your application, and the nature of your data source.

At this point you should be seriously rethinking your decision to do this in Bash. Other languages already have features that take care of all these issues for you, and you may be ''much'' better off writing your application in one of those languages instead.

You're still here? OK. Let's suppose we're going to use the `/dev/urandom` device (found on most Linux and BSD systems) as an external random data source in Bash. This is a character device which produces raw bytes of "pretty random" data. First, we'll note that the script will only work on systems where this is present. In fact, you should add an explicit check for this device somewhere early in the script, and abort if it's not found.

Now, how can we turn these bytes of data into numbers for Bash? If we attempt to read a byte into a variable, a NUL byte would give us a variable which appears to be empty. However, since no other input gives us that result, this may be acceptable -- an empty variable means we tried to read a NUL. We can work with this. The good news is we won't have to fork an `od(1)` or any other external program to read bytes. Then, since we're reading one byte at a time, this also means we don't have to write any prefetching or buffering code to save forks.

One other gotcha, however: reading bytes only works in the C [[locale]]. If we try this in `en_US.utf8` we get an empty variable for every byte from 128 to 255, which is clearly no good.

So, let's put this all together and see what we've got:

{{{#!highlight bash
#!/usr/bin/env bash
# Requires Bash 3.1 or higher, and an OS with /dev/urandom (Linux, BSD, etc.)

export LANG=C
if [[ ! -e /dev/urandom ]]; then
    echo "No /dev/urandom on this system" >&2
    exit 1
fi

# Return an unbiased random number from 0 to ($1 - 1) in variable 'r'.
rand() {
    if (($1 > 256)); then
        echo "Argument larger than 256 currently unsupported" >&2
 r=-1
 return 1
    fi

    local max=$((256 / $1 * $1))
    while IFS= read -r -n1 '' r < /dev/urandom
          printf -v r %d "'$r"
   ((r >= max))
    do
        :
    done
    r=$((r % $1))
}
Line 102: Line 175:
For more details, see `info coreutils sort` or an equivalent manual. This uses a trick from [[BashFAQ/071|FAQ 71]] for converting bytes to numbers. When the variable populated by `read` is empty (because of a NUL byte), we get 0, which is just what we want.
Line 104: Line 177:
 > --([[http://lists.gnu.org/archive/html/bug-bash/2010-01/msg00042.html]] points out a surprising pitfall concerning the use of `RANDOM` without a leading `$` in certain mathematical contexts. (Upshot: you should prefer `n=$((...math...)); ((array[n]++))` over `((array[...math...]++))` in almost every case.))--

Behavior described appears reversed in current versions of mksh, ksh93, Bash, and Zsh. Still something to keep in mind for legacy. -ormaaj
Extending this to handle ranges larger than 0..255 is left as an exercise for the reader.

How can I randomize (shuffle) the order of lines in a file? Or select a random line from a file, or select a random file from a directory?

To randomize the lines of a file, here is one approach. This one involves generating a random number, which is prefixed to each line; then sorting the resulting lines, and removing the numbers.

# Bash/Ksh
randomize() {
    while IFS='' read -r l ; do printf '%d\t%s\n' "$RANDOM" "$l"; done |
    sort -n |
    cut -f2-
}

RANDOM is supported by BASH and KornShell, but is not defined by POSIX.

Here's the same idea (printing random numbers in front of a line, and sorting the lines on that column) using other programs:

# Bourne
awk '
    BEGIN { srand() }
    { print rand() "\t" $0 }
' |
sort -n |    # Sort numerically on first (random number) column
cut -f2-     # Remove sorting column

This is (possibly) faster than the previous solution, but will not work for very old AWK implementations (try nawk, or gawk, or /usr/xpg4/bin/awk if available). (Note that AWK uses the epoch time as a seed for srand(), which may or may not be random enough for you.)

Other non-portable utilities that can shuffle/randomize a file:

  • GNU shuf (in recent enough GNU coreutils)

  • GNU sort -R (coreutils 6.9)

Recent GNU sort has the -R (aka --random-sort) flag. Oddly enough, it only works for the generic locale:

LC_ALL=C sort -R file     # output the lines in file in random order
LC_ALL=POSIX sort -R file # output the lines in file in random order
LC_ALL=en_US sort -R file # effectively ignores the -R option

For more details, see info coreutils sort or an equivalent manual.

Shuffling an array

A generalized version of this question might be, How can I shuffle the elements of an array? If we don't want to use the rather clumsy approach of sorting lines, this is actually more complex than it appears. A naive approach would give us badly biased results. A more complex (and correct) algorithm looks like this:

# Uses a global array variable.  Must be compact (not a sparse array).
# Bash syntax.
shuffle() {
   local i tmp size max rand

   # $RANDOM % (i+1) is biased because of the limited range of $RANDOM
   # Compensate by using a range which is a multiple of the array size.
   size=${#array[*]}
   max=$(( 32768 / size * size ))

   for ((i=size-1; i>0; i--)); do
      while (( (rand=$RANDOM) >= max )); do :; done
      rand=$(( rand % (i+1) ))
      tmp=${array[i]} array[i]=${array[rand]} array[rand]=$tmp
   done
}

This function shuffles the elements of an array in-place using the Knuth-Fisher-Yates shuffle algorithm.

If we just want the unbiased random number picking function, we can split that out separately:

# Returns random number from 0 to ($1-1) in global var 'r'.
# Bash syntax.
rand() {
    local max=$((32768 / $1 * $1))
    while (( (r=$RANDOM) >= max )); do :; done
    r=$(( r % $1 ))
}

This rand function is better than using $((RANDOM % n)). For simplicity, many of the remaining examples on this page may use the modulus approach. In all such cases, switching to the use of the rand function will give better results; this improvement is left as an exercise for the reader.

Selecting a random line/file

Another question we frequently see is, How can I print a random line from a file? The problem here is that you need to know in advance how many lines the file contains. Lacking that knowledge, you have to read the entire file through once just to count them -- or, you have to suck the entire file into memory. Let's explore both of these approaches.

# Bash
n=$(wc -l <"$file")         # Count number of lines.
r=$((RANDOM % n + 1))       # Random number from 1..n (see warnings above!)
sed -n "$r{p;q;}" "$file"   # Print the r'th line.

# POSIX with (new) AWK
awk -v n="$(wc -l <"$file")" \
  'BEGIN{srand();l=int((rand()*n)+1)} NR==l{print;exit}' "$file"

(See FAQ 11 for more info about printing the r'th line.)

The next example sucks the entire file into memory. This approach saves time rereading the file, but obviously uses more memory. (Arguably: on systems with sufficient memory and an effective disk cache, you've read the file into memory by the earlier methods, unless there's insufficient memory to do so, in which case you shouldn't, QED.)

# Bash
unset lines n
while IFS= read -r 'lines[n++]'; do :; done < "$file"   # See FAQ 5
r=$((RANDOM % n))   # See warnings above!
echo "${lines[r]}"

Note that we don't add 1 to the random number in this example, because the array of lines is indexed counting from 0.

Also, some people want to choose a random file from a directory (for a signature on an e-mail, or to choose a random song to play, or a random image to display, etc.). A similar technique can be used:

# Bash
files=(*.ogg)                  # Or *.gif, or *
n=${#files[@]}                 # For readability
xmms -- "${files[RANDOM % n]}" # Choose a random element

Known bugs

  • http://lists.gnu.org/archive/html/bug-bash/2010-01/msg00042.html points out a surprising pitfall concerning the use of RANDOM without a leading $ in certain mathematical contexts. (Upshot: you should prefer n=$((...math...)); ((array[n]++)) over ((array[...math...]++)) in almost every case.)

    • Behavior described appears reversed in current versions of mksh, ksh93, Bash, and Zsh. Still something to keep in mind for legacy. -ormaaj

Using external random data sources

Some people feel the shell's builtin RANDOM parameter is not sufficiently random for their applications. Typically this will be an interface to the C library's rand(3) function, although the Bash manual does not specify the implementation details. Some people feel their application requires cryptographically stronger random data, which would have to be supplied by some external source.

Before we explore this, we should point out that often people want to do this as a first step in writing some sort of random password generator. If that is your goal, you should at least consider using a password generator that has already been written, such as pwgen.

Now, if we're considering the use of external random data sources in a Bash script, we face several issues:

  • The data source will probably not be portable. Thus, the script will only be usable in special environments.
  • If we simply grab a byte (or a single group of bytes large enough to span the desired range) from the data source and do a modulus on it, we will run into the bias issue described earlier on this page. There is absolutely no point in using an expensive external data source if we're just going to bias the results with sloppy code! To work around that, we may need to grab bytes (or groups of bytes) repeatedly, until we get one that can be used without bias.
  • Bash can't handle raw bytes very well, so each time we grab a byte (or group) we need to do something to it to turn it into a number that Bash can read. This may be an expensive operation. So, it may be more efficient to grab several bytes (or groups), and do the conversion to readable numbers, all at once.

  • Depending on the data source, these random bytes may be precious, so grabbing a lot of them all at once and discarding ones we don't use might be more expensive (by whatever metric we're using to measure such costs) than grabbing one byte at a time, even counting the conversion step. This is something you'll have to decide for yourself, taking into account the needs of your application, and the nature of your data source.

At this point you should be seriously rethinking your decision to do this in Bash. Other languages already have features that take care of all these issues for you, and you may be much better off writing your application in one of those languages instead.

You're still here? OK. Let's suppose we're going to use the /dev/urandom device (found on most Linux and BSD systems) as an external random data source in Bash. This is a character device which produces raw bytes of "pretty random" data. First, we'll note that the script will only work on systems where this is present. In fact, you should add an explicit check for this device somewhere early in the script, and abort if it's not found.

Now, how can we turn these bytes of data into numbers for Bash? If we attempt to read a byte into a variable, a NUL byte would give us a variable which appears to be empty. However, since no other input gives us that result, this may be acceptable -- an empty variable means we tried to read a NUL. We can work with this. The good news is we won't have to fork an od(1) or any other external program to read bytes. Then, since we're reading one byte at a time, this also means we don't have to write any prefetching or buffering code to save forks.

One other gotcha, however: reading bytes only works in the C locale. If we try this in en_US.utf8 we get an empty variable for every byte from 128 to 255, which is clearly no good.

So, let's put this all together and see what we've got:

   1 #!/usr/bin/env bash
   2 # Requires Bash 3.1 or higher, and an OS with /dev/urandom (Linux, BSD, etc.)
   3 
   4 export LANG=C
   5 if [[ ! -e /dev/urandom ]]; then
   6     echo "No /dev/urandom on this system" >&2
   7     exit 1
   8 fi
   9 
  10 # Return an unbiased random number from 0 to ($1 - 1) in variable 'r'.
  11 rand() {
  12     if (($1 > 256)); then
  13         echo "Argument larger than 256 currently unsupported" >&2
  14         r=-1
  15         return 1
  16     fi
  17 
  18     local max=$((256 / $1 * $1))
  19     while IFS= read -r -n1 '' r < /dev/urandom
  20           printf -v r %d "'$r"
  21           ((r >= max))
  22     do
  23         :
  24     done
  25     r=$((r % $1))
  26 }

This uses a trick from FAQ 71 for converting bytes to numbers. When the variable populated by read is empty (because of a NUL byte), we get 0, which is just what we want.

Extending this to handle ranges larger than 0..255 is left as an exercise for the reader.


CategoryShell

BashFAQ/026 (last edited 2022-01-30 23:49:34 by emanuele6)