La mejor manera de elegir un archivo aleatorio de un directorio en un script de shell


¿Cuál es la mejor manera de elegir un archivo aleatorio de un directorio en un script de shell?

Aquí está mi solución en Bash, pero estaría muy interesado en una versión más portátil (no GNU) para su uso en 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"

Alguien tiene alguna otra idea?

Editar: lhunath hace un buen punto sobre el análisis ls. Supongo que todo se reduce a si quieres ser portátil o no. Si tienes los findutils y coreutils de GNU, entonces puedes hacer:

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

Whew, eso fue divertido! También coincide mejor con mi pregunta ya que dije "archivo aleatorio". Honsetly sin embargo, en estos días es difícil imaginar un sistema Unix desplegado por ahí con GNU instalado, pero no Perl 5.

Author: JasonSmith, 2009-03-31

11 answers

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

Y no analicen ls. Leer http://mywiki.wooledge.org/ParsingLs

Editar: Buena suerte encontrando una solución no-bash que sea confiable. La mayoría se romperá para ciertos tipos de nombres de archivo, como los nombres de archivo con espacios o líneas nuevas o guiones (es casi imposible en pure sh). Para hacerlo bien sin bash, tendría que migrar completamente a awk/perl/python/... sin tubería que la salida para el procesamiento posterior o tal.

 56
Author: lhunath,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-19 12:23:30

¿No es "shuf" portátil?

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

O buscar si los archivos son más profundos que un directorio:

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

Es parte de coreutils, pero necesitará 6.4 o más reciente para obtenerlo... así que RH / CentOS no lo incluye.

 24
Author: johnnyB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-04-02 01:57:06

Algo lile "

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

RANDOM RANDOM in bash es una variable especial que devuelve un número aleatorio, luego usa la división de módulo para obtener un índice válido, luego indexa en la matriz.

 3
Author: fido,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-31 15:22:38
# ******************************************************************
# ******************************************************************
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
Author: Pipo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-06 11:31:03

Esto se reduce a: ¿Cómo puedo crear un número aleatorio en un script Unix de forma portátil?

Porque si tienes un número aleatorio entre 1 y N, puedes usar head -$N | tail para cortar en algún lugar del medio. Desafortunadamente, no conozco una forma portátil de hacer esto solo con el caparazón. Si tiene Python o Perl, puede usar fácilmente su soporte aleatorio, pero AFAIK, no hay un comando estándar rand(1).

 2
Author: Aaron Digulla,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-31 15:26:31

Creo que Awk es una buena herramienta para obtener un número aleatorio. De acuerdo con Advanced Bash Guide, Awk es un buen reemplazo de números aleatorios para $RANDOM.

Aquí hay una versión de su script que evita Bash-isms y herramientas 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"

Hereda los problemas que otras respuestas han mencionado si los archivos contienen nuevas líneas.

 2
Author: ashawley,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-31 17:37:44

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

Su idea casi funcionó, pero tuve que agregar un [@]

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

 2
Author: Marcelo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-17 05:51:17

Las nuevas líneas en los nombres de archivo se pueden evitar haciendo lo siguiente en 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
Author: gsbabil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-06-26 12:15:14

Aquí hay un fragmento de shell que se basa solo en características POSIX y hace frente a nombres de archivo arbitrarios (pero omite los archivos dot de la selección). La selección aleatoria usa awk, porque eso es todo lo que obtienes en POSIX. Es un generador de números aleatorios muy pobre, ya que el RNG de awk se siembra con el tiempo actual en segundos (por lo que es fácilmente predecible, y devuelve la misma opción si lo llamas varias veces por segundo).

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

Si no desea ignorar los archivos dot, la generación del nombre del archivo el código (set -- *) necesita ser reemplazado por algo más complicado.

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 tiene OpenSSL disponible, puede usarlo para generar bytes aleatorios. Si no lo hace, pero su sistema tiene /dev/urandom, reemplace la llamada a openssl por dd if=/dev/urandom bs=3 count=1 2>/dev/null. Aquí hay un fragmento que establece n a un valor aleatorio entre 1 y $#, teniendo cuidado de no introducir un sesgo. Este fragmento asume que $# es como máximo 2^23-1.

while
  n=$(($(openssl rand 3 | od -An -t u4) + 1))
  [ $n -gt $((16777216 / $# * $#)) ]
do :; done
n=$((n % $#))
 1
Author: Gilles,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-07-19 12:55:09

BusyBox (usado en dispositivos embebidos) usualmente está configurado para soportar $RANDOM pero no tiene arrays de estilo bash o sort --random-sort o shuf. De ahí lo siguiente:

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

Tenga en cuenta "-" al final en cut -f2-; esto es necesario para evitar truncar archivos que contienen espacios (o cualquier separador que desee usar).

No manejará correctamente los nombres de archivo con nuevas líneas incrustadas.

 0
Author: Robert Calhoun,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-04-08 17:04:46

Coloque cada línea de salida del comando 'ls' en una matriz asociativa llamada line y luego elija una de esas como esta...

ls | awk '{ line[NR]=$0 } END { print line[(int(rand()*NR+1))]}'
 0
Author: kapu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-18 04:07:04