Best way to select random file from directory in shell script

What is the best way to select a random file from a directory in a shell script?

Here is my solution in Bash, but I would be very interested in using a more portable (non-GNU) version for use on Unix.

dir='some/directory' file=`/bin/ls -1 "$dir" | sort --random-sort | head -1` path=`readlink --canonicalize "$dir/$file"` # Converts to full path echo "The randomly-selected file is: $path" 

Does anyone have any other ideas?

Edit: lhunath gives a good idea on parsing ls . I guess it comes to whether you want to be portable or not. If you have GNU findutils and coreutils, you can:

 find "$dir" -maxdepth 1 -mindepth 1 -type f -print0 \ | sort --zero-terminated --random-sort \ | sed 's/\d000.*//g/' 

Well, that was fun! Also it fits my question better since I said "random file". However, these days it is hard to imagine that a Unix system is deployed where GNU is installed, but not Perl 5.

+41
file bash shell random
Mar 31 '09 at 15:12
source share
11 answers
 files=(/my/dir/*) printf "%s\n" "${files[RANDOM % ${#files[@]}]}" 

And do not parse ls . Read http://mywiki.wooledge.org/ParsingLs

Edit: Good luck finding a bash solution that is reliable. Most will break for certain types of file names, such as file names with spaces or newlines or dashes (this is almost impossible in pure sh ). To do this correctly without bash , you need to completely switch to awk / perl / python / ... without a pipeline that will be output for further processing or such.

+55
Mar 31 '09 at 16:49
source share
— -

Is "shuf" not portable?

 shuf -n1 -e /path/to/files/* 

or find if the files are deeper than one directory:

 find /path/to/files/ -type f | shuf -n1 

it is part of coreutils, but you will need 6.4 or later to get it ... therefore RH / CentOS does not include it.

+21
Apr 02 '13 at 1:57
source share

Something lile "

 let x="$RANDOM % ${#file}" echo "The randomly-selected file is ${path[$x]}" 

$ RANDOM in bash is a special variable that returns a random number, and then uses modular division to get a valid index, and then indexes it into an array.

+3
Mar 31 '09 at 15:22
source share
 # ****************************************************************** # ****************************************************************** function randomFile { tmpFile=$(mktemp) files=$(find . -type f > $tmpFile) total=$(cat "$tmpFile"|wc -l) randomNumber=$(($RANDOM%$total)) i=0 while read line; do if [ "$i" -eq "$randomNumber" ];then # Do stuff with file amarok $line break fi i=$[$i+1] done < $tmpFile rm $tmpFile } 
+3
Nov 06 '12 at 11:31
source share

It boils down to: How can I create a random number in a Unix script in a portable way?

Because if you have a random number between 1 and N, you can use head -$N | tail head -$N | tail to cut somewhere in the middle. Unfortunately, I do not know a portable way to do this with the shell only. If you have Python or Perl, you can easily use their random support, but AFAIK, there is no standard rand(1) command.

+2
Mar 31 '09 at 15:26
source share

I think Awk is a good tool for getting a random number. According to the Advanced Bash Guide , Awk is a good random number replacement for $RANDOM .

Here is a version of your script that avoids Bash -ism and GNU tools.

 #! /bin/sh dir='some/directory' n_files=`/bin/ls -1 "$dir" | wc -l | cut -f1` rand_num=`awk "BEGIN{srand();print int($n_files * rand()) + 1;}"` file=`/bin/ls -1 "$dir" | sed -ne "${rand_num}p"` path=`cd $dir && echo "$PWD/$file"` # Converts to full path. echo "The randomly-selected file is: $path" 

It inherits the problems that other answers talked about if the files contain newlines.

+2
Mar 31 '09 at 17:37
source share

files=(/my/dir/*) printf "%s\n" "${files[RANDOM % ${#files}]}"

Your idea almost worked, but I had to add [@]

files=(/my/dir/*) printf "%s\n" "${files[RANDOM % ${#files[@]}]}"

+2
Jan 17 2018-11-11T00:
source share

New lines in file names can be avoided by doing the following in Bash:

 #!/bin/sh OLDIFS=$IFS IFS=$(echo -en "\n\b") DIR="/home/user" for file in $(ls -1 $DIR) do echo $file done IFS=$OLDIFS 
+2
Jun 26 '11 at 12:15
source share

Here is a shell fragment that relies only on POSIX functions and handles arbitrary file names (but skips point files from selection). Random selection uses awk because everything you get in POSIX. This is a very bad random number generator because awk RNG is seeded by the current time in seconds (therefore, it is easily predictable and returns the same choice if you call it several times per second).

 set -- * n=$(echo $# | awk '{srand(); print int(rand()*$0) + 1}') eval "file=\$$n" echo "Processing $file" 

If you do not want to ignore point files, the file name generation code ( set -- * ) should be replaced with something more complex.

 set -- *; [ -e "$1" ] || shift set .[!.]* "$@"; [ -e "$1" ] || shift set ..?* "$@"; [ -e "$1" ] || shift if [ $# -eq 0]; then echo 1>&2 "empty directory"; exit 1; fi 

If you have OpenSSL, you can use it to generate random bytes. If you do not, but your system has /dev/urandom , replace the call to openssl with dd if=/dev/urandom bs=3 count=1 2>/dev/null . Here is a snippet that sets n to a random value between 1 and $# , trying not to enter an offset. This snippet assumes $# at most 2 ^ 23-1.

 while n=$(($(openssl rand 3 | od -An -t u4) + 1)) [ $n -gt $((16777216 / $# * $#)) ] do :; done n=$((n % $#)) 
+1
Jul 19 '11 at 12:55
source share

BusyBox (used on embedded devices) is usually configured to support $RANDOM , but it does not have bash-line arrays or sort --random-sort or shuf . It follows the following:

 #!/bin/sh FILES="/usr/bin/*" for f in $FILES; do echo "$RANDOM $f" ; done | sort -n | head -n1 | cut -d' ' -f2- 

Note: "-" in cut -f2- ; this is necessary to prevent trimming of files containing spaces (or any delimiter you want to use).

It will not correctly handle file names with embedded newlines.

0
Apr 08 '15 at 17:04 on
source share

Put each line of output from the 'ls' command into an associative array named line, and then select one of them, for example ...

 ls | awk '{ line[NR]=$0 } END { print line[(int(rand()*NR+1))]}' 
0
Feb 16 '16 at 10:44
source share



All Articles