Meilleur moyen de choisir un fichier aléatoire dans un répertoire dans un script shell

Quelle est la meilleure façon de choisir un fichier aléatoire dans un répertoire dans un script shell?

Voici ma solution dans Bash mais je serais très intéressé par une version plus portable (non GNU) à utiliser sur Unix proprement dit.

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" 

Quelqu’un a-t-il d’autres idées?

Edit: lhunath fait une bonne remarque sur l’parsing syntaxique de ls . Je suppose que cela revient à savoir si vous voulez être portable ou non. Si vous avez les findutils et coreutils GNU, alors vous pouvez faire:

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

Ouf, c’était amusant! Aussi, cela correspond mieux à ma question puisque j’ai dit “fichier aléatoire”. Honnêtement, ces jours-ci, il est difficile d’imaginer un système Unix déployé avec GNU installé, mais pas Perl 5.

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

Et n’parsing pas les ls . Lisez http://mywiki.wooledge.org/ParsingLs

Edit: Bonne chance pour trouver une solution non fiable qui soit fiable. La plupart vont se briser pour certains types de noms de fichiers, tels que les noms de fichiers comportant des espaces ou des nouvelles lignes ou des tirets (il est quasiment impossible de les sh dans pure sh ). Pour le faire sans bash , vous devez migrer complètement vers awk / perl / python / … sans redirect cette sortie pour un traitement ultérieur ou autre.

Est-ce que “shuf” n’est pas portable?

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

ou trouvez si les fichiers sont plus profonds qu’un répertoire:

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

ça fait partie de coreutils mais vous aurez besoin de 6.4 ou plus pour l’obtenir … alors RH / CentOS ne l’inclut pas.

Quelque chose ”

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

$ RANDOM dans bash est une variable spéciale qui renvoie un nombre aléatoire, puis utilise la division modulus pour obtenir un index valide, puis indexe dans le tableau.

 # ****************************************************************** # ****************************************************************** 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 } 

Cela se résume à: Comment puis-je créer un nombre aléatoire dans un script Unix de manière portable?

Parce que si vous avez un nombre aléatoire entre 1 et N, vous pouvez utiliser head -$N | tail head -$N | tail à couper quelque part au milieu. Malheureusement, je ne connais aucun moyen portable de le faire avec le shell seul. Si vous avez Python ou Perl, vous pouvez facilement utiliser leur support aléatoire mais AFAIK, il n’y a pas de commande rand(1) standard.

Je pense que Awk est un bon outil pour obtenir un nombre aléatoire. Selon le guide Bash avancé , Awk est un bon remplacement de nombre aléatoire pour $RANDOM .

Voici une version de votre script qui évite les outils Bash-isms et GNU.

 #! /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" 

Il hérite des problèmes mentionnés dans les autres réponses si les fichiers contiennent des nouvelles lignes.

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

Votre idée a presque fonctionné, mais j’ai dû append un [@]

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

Les nouvelles lignes dans les noms de fichiers peuvent être évitées en procédant comme suit dans 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 

Voici un extrait de shell qui ne repose que sur les fonctionnalités POSIX et s’adapte à des noms de fichiers arbitraires (mais omet les fichiers de points de la sélection). La sélection aléatoire utilise awk, car c’est tout ce que vous obtenez dans POSIX. C’est un générateur de nombres aléatoires très médiocre, car le RNG de awk est chargé avec l’heure actuelle en secondes (il est donc facilement prévisible et renvoie le même choix si vous l’appelez plusieurs fois par seconde).

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

Si vous ne voulez pas ignorer les fichiers de points, le code de génération de nom de fichier ( set -- * ) doit être remplacé par quelque chose de plus compliqué.

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

Si OpenSSL est disponible, vous pouvez l’utiliser pour générer des octets aléatoires. Si vous ne le faites pas, mais que votre système a /dev/urandom , remplacez l’appel à openssl par dd if=/dev/urandom bs=3 count=1 2>/dev/null . Voici un extrait qui définit n à une valeur aléatoire comprise entre 1 et $# , en prenant soin de ne pas introduire de biais. Cet extrait de code suppose que $# est au plus 2 ^ 23-1.

 while n=$(($(openssl rand 3 | od -An -t u4) + 1)) [ $n -gt $((16777216 / $# * $#)) ] do :; done n=$((n % $#)) 

BusyBox (utilisé sur les périphériques embarqués) est généralement configuré pour prendre en charge $RANDOM mais il n’a pas de tableaux de type bash ou de sort --random-sort ou shuf . D’où le suivant:

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

Remarque “-” à la fin de la cut -f2- ; Cela est nécessaire pour éviter de tronquer des fichiers contenant des espaces (ou le séparateur que vous souhaitez utiliser).

Il ne gérera pas correctement les noms de fichiers avec les nouvelles lignes intégrées.

Mettez chaque ligne de sortie de la commande ‘ls’ dans un tableau associatif nommé line puis choisissez l’un de ceux comme ça …

 ls | awk '{ line[NR]=$0 } END { print line[(int(rand()*NR+1))]}'