web-dev-qa-db-fra.com

Meilleure façon de choisir un fichier aléatoire dans un répertoire d'un script Shell

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

Voici ma solution dans Bash mais je serais très intéressé par une version plus portable (non GNU) pour une utilisation 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 d'autres idées?

Edit: lhunath fait un bon point sur l'analyse de ls. Je suppose que cela revient à savoir si vous voulez être portable ou non. Si vous avez les GNU findutils et coreutils 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! De plus, cela correspond mieux à ma question puisque j'ai dit "fichier aléatoire". Honnêtement cependant, de nos jours, il est difficile d'imaginer un système Unix déployé là-bas ayant GNU installé mais pas Perl 5.

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

Et ne pas analyser ls . Lire http://mywiki.wooledge.org/ParsingLs

Edit: Bonne chance pour trouver une solution nonbash fiable. La plupart des casse pour certains types de noms de fichiers, tels que les noms de fichiers avec des espaces ou des retours à la ligne ou des tirets (c'est pratiquement impossible en pur sh). Pour le faire correctement sans bash, vous devez migrer complètement vers awk/Perl/python/... sans canaliser cette sortie pour plus traitement ou autre.

58
lhunath

"Shuf" n'est-il 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

cela fait partie de coreutils mais vous aurez besoin de la version 6.4 ou plus récente pour l'obtenir ... donc RH/CentOS ne l'inclut pas.

30
johnnyB
# ******************************************************************
# ******************************************************************
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
Pipo

Quelque chose comme:

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

$RANDOM en bash est une variable spéciale qui renvoie un nombre aléatoire, puis j'utilise la division du module pour obtenir un index valide, puis référence cet index dans le tableau.

3
fido

Voici un extrait de shell qui repose uniquement sur les fonctionnalités POSIX et gère les 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 très mauvais générateur de nombres aléatoires, car le RNG de awk est initialisé 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 dot, 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 ce n'est pas le cas, 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 sur une valeur aléatoire comprise entre 1 et $#, en veillant à ne pas introduire de biais. Cet extrait 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 % $#))
2
Gilles

Les sauts de ligne dans les noms de fichiers peuvent être évités 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
2
gsbabil

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 Pour 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 standard Rand(1).

2
Aaron Digulla

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

Voici une version de votre script qui évite les Bash-isms et les outils 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 par d'autres réponses si les fichiers contiennent des retours à la ligne.

2
ashawley

BusyBox (utilisé sur les appareils intégrés) est généralement configuré pour prendre en charge $RANDOM mais il n'a pas de tableaux de style bash ou sort --random-sort ou shuf. D'où ce qui suit:

#!/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 cut -f2-; cela est nécessaire pour éviter de tronquer les fichiers contenant des espaces (ou le séparateur que vous souhaitez utiliser).

Il ne traitera pas correctement les noms de fichiers avec des sauts de ligne intégrés.

1
Robert Calhoun

Mettez chaque ligne de sortie de la commande 'ls' dans un tableau associatif nommé ligne, puis choisissez-en une comme celle-ci ...

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

Mes 2 cents, avec une version qui ne devrait pas se casser lorsque des noms de fichiers avec des caractères spéciaux existent:

#!/bin/bash --
dir='some/directory'

let number_of_files=$(find "${dir}" -type f -print0 | grep -zc .)
let Rand_index=$((1+(RANDOM % number_of_files)))

printf "the randomly-selected file is: "
find "${dir}" -type f -print0 | head -z -n "${Rand_index}" | tail -z -n 1
printf "\n"
0
Jay jargot