Tengo un directorio con aproximadamente 2000 archivos. ¿Cómo puedo seleccionar una muestra aleatoria de N
archivos mediante el uso de un script bash o una lista de comandos canalizados?
ls | shuf -n 5
Fuente de Unix Stackexchange
Tengo un directorio con aproximadamente 2000 archivos. ¿Cómo puedo seleccionar una muestra aleatoria de N
archivos mediante el uso de un script bash o una lista de comandos canalizados?
ls | shuf -n 5
Fuente de Unix Stackexchange
Respuestas:
Aquí hay un script que usa la opción aleatoria de GNU sort:
ls |sort -R |tail -$N |while read file; do
# Something involving $file, or you can leave
# off the while to just get the filenames
done
"$file"
, no mostrado, sería sensible a los espacios.
Puede usar shuf
(del paquete GNU coreutils) para eso. Simplemente alimente una lista de nombres de archivos y pídale que devuelva la primera línea de una permutación aleatoria:
ls dirname | shuf -n 1
# probably faster and more flexible:
find dirname -type f | shuf -n 1
# etc..
Ajuste el -n, --head-count=COUNT
valor para devolver el número de líneas deseadas. Por ejemplo, para devolver 5 nombres de archivo aleatorios que usaría:
find dirname -type f | shuf -n 5
N
archivos aleatorios, por lo que usar 1
es un poco engañoso.
find dirname -type f -print0 | shuf -zn1
Aquí hay algunas posibilidades que no analizan la salida ls
y que son 100% seguras con respecto a los archivos con espacios y símbolos divertidos en su nombre. Todos ellos llenarán una matriz randf
con una lista de archivos aleatorios. Esta matriz se imprime fácilmente printf '%s\n' "${randf[@]}"
si es necesario.
Este posiblemente generará el mismo archivo varias veces y N
debe conocerse de antemano. Aquí elegí N = 42.
a=( * )
randf=( "${a[RANDOM%${#a[@]}]"{1..42}"}" )
Esta característica no está muy bien documentada.
Si N no se conoce de antemano, pero realmente le gustó la posibilidad anterior, puede usar eval
. Pero es malo, ¡y realmente debes asegurarte de que N
no provenga directamente de la entrada del usuario sin ser revisado a fondo!
N=42
a=( * )
eval randf=( \"\${a[RANDOM%\${#a[@]}]\"\{1..$N\}\"}\" )
Personalmente no me gusta eval
y de ahí esta respuesta!
Lo mismo con un método más directo (un bucle):
N=42
a=( * )
randf=()
for((i=0;i<N;++i)); do
randf+=( "${a[RANDOM%${#a[@]}]}" )
done
Si no quieres tener varias veces el mismo archivo:
N=42
a=( * )
randf=()
for((i=0;i<N && ${#a[@]};++i)); do
((j=RANDOM%${#a[@]}))
randf+=( "${a[j]}" )
a=( "${a[@]:0:j}" "${a[@]:j+1}" )
done
Nota . Esta es una respuesta tardía a una publicación anterior, pero la respuesta aceptada enlaza con una página externa que muestra un terribleintentopráctica, y la otra respuesta no es mucho mejor ya que también analiza la salida de ls
. Un comentario a la respuesta aceptada apunta a una excelente respuesta de Lhunath que obviamente muestra una buena práctica, pero no responde exactamente al OP.
"{1..42}"
parte dejara un rastro "1"
. Además, $RANDOM
solo tiene 15 bits y el método no funcionará con más de 32767 archivos para elegir.
ls | shuf -n 10 # ten random files
ls
. Esto no funcionará si, por ejemplo, un nombre de archivo contiene nuevas líneas.
ls
no se garantiza que le dé nombres de archivo "limpios", por lo que no debe confiar en él, punto. El hecho de que estos problemas sean raros o inusuales no cambia el problema; especialmente dado que hay mejores soluciones para esto.
ls
puede incluir directorios y líneas en blanco. Sugeriría algo así en su find . -type f | shuf -n10
lugar.
Una solución simple para seleccionar 5
archivos aleatorios y evitar analizar ls . También funciona con archivos que contienen espacios, líneas nuevas y otros caracteres especiales:
shuf -ezn 5 * | xargs -0 -n1 echo
Reemplace echo
con el comando que desea ejecutar para sus archivos.
read
tiene los mismos problemas que el análisis ls
? a saber, lee línea por línea, por lo que no funciona para archivos con nuevas líneas en su nombre
Si tiene instalado Python (funciona con Python 2 o Python 3):
Para seleccionar un archivo (o línea de un comando arbitrario), use
ls -1 | python -c "import sys; import random; print(random.choice(sys.stdin.readlines()).rstrip())"
Para seleccionar N
archivos / líneas, use (la nota se N
encuentra al final del comando, reemplácela por un número)
ls -1 | python -c "import sys; import random; print(''.join(random.sample(sys.stdin.readlines(), int(sys.argv[1]))).rstrip())" N
Esta es una respuesta aún más tardía a la respuesta tardía de @ gniourf_gniourf, que acabo de votar porque es, con mucho, la mejor respuesta, dos veces. (Una vez para evitar eval
y una vez para el manejo seguro de nombre de archivo).
Pero me tomó unos minutos desenredar las características "no muy bien documentadas" que utiliza esta respuesta. Si sus habilidades de Bash son lo suficientemente sólidas como para que haya visto de inmediato cómo funciona, omita este comentario. Pero no lo hice, y habiéndolo desenredado creo que vale la pena explicarlo.
La función n. ° 1 es el bloqueo de archivos del propio shell a=(*)
crea una matriz, $a
cuyos miembros son los archivos en el directorio actual. Bash comprende todas las rarezas de los nombres de archivos, por lo que la lista está garantizada como correcta, garantizada como escape, etc. No es necesario preocuparse por analizar correctamente los nombres de archivo textuales devueltos por ls
.
La característica # 2 es expansiones de parámetros Bash para matrices , una anidada dentro de otra. Esto comienza con ${#ARRAY[@]}
, que se expande a lo largo de $ARRAY
.
Esa expansión se utiliza para subíndice de la matriz. La manera estándar de encontrar un número aleatorio entre 1 y N es tomar el valor del número aleatorio módulo N. Queremos un número aleatorio entre 0 y la longitud de nuestra matriz. Aquí está el enfoque, dividido en dos líneas para mayor claridad:
LENGTH=${#ARRAY[@]}
RANDOM=${a[RANDOM%$LENGTH]}
Pero esta solución lo hace en una sola línea, eliminando la asignación de variables innecesarias.
La característica n. ° 3 es la expansión de la llave Bash , aunque debo confesar que no la entiendo completamente. Se utiliza la expansión de llaves, por ejemplo, para generar una lista de 25 archivos con el nombre filename1.txt
, filename2.txt
etc: echo "filename"{1..25}".txt"
.
La expresión dentro del subshell anterior, "${a[RANDOM%${#a[@]}]"{1..42}"}"
usa ese truco para producir 42 expansiones separadas. La expansión de la llave coloca un solo dígito entre el ]
y el }
, que al principio pensé que estaba suscribiendo la matriz, pero de ser así estaría precedido por dos puntos. (También habría devuelto 42 elementos consecutivos desde un punto aleatorio en la matriz, lo que no es lo mismo que devolver 42 elementos aleatorios de la matriz). Creo que solo está haciendo que el shell ejecute la expansión 42 veces, devolviendo así 42 artículos al azar de la matriz. (Pero si alguien puede explicarlo más completamente, me encantaría escucharlo).
La razón por la que N tiene que estar codificado (a 42) es que la expansión de llaves ocurre antes de la expansión variable.
Finalmente, aquí está la Característica # 4 , si desea hacer esto de forma recursiva para una jerarquía de directorios:
shopt -s globstar
a=( ** )
Esto activa una opción de shell que hace **
que coincida recursivamente. Ahora su $a
matriz contiene todos los archivos en toda la jerarquía.
Si tiene más archivos en su carpeta, puede usar el siguiente comando entubado que encontré en unix stackexchange .
find /some/dir/ -type f -print0 | xargs -0 shuf -e -n 8 -z | xargs -0 cp -vt /target/dir/
Aquí quería copiar los archivos, pero si desea mover archivos o hacer otra cosa, simplemente cambie el último comando donde lo he usado cp
.
Este es el único script que puedo jugar bien con bash en MacOS. Combiné y edité fragmentos de los siguientes dos enlaces:
Comando ls: ¿cómo puedo obtener una lista de ruta completa recursiva, una línea por archivo?
#!/bin/bash
# Reads a given directory and picks a random file.
# The directory you want to use. You could use "$1" instead if you
# wanted to parametrize it.
DIR="/path/to/"
# DIR="$1"
# Internal Field Separator set to newline, so file names with
# spaces do not break our script.
IFS='
'
if [[ -d "${DIR}" ]]
then
# Runs ls on the given dir, and dumps the output into a matrix,
# it uses the new lines character as a field delimiter, as explained above.
# file_matrix=($(ls -LR "${DIR}"))
file_matrix=($(ls -R $DIR | awk '; /:$/&&f{s=$0;f=0}; /:$/&&!f{sub(/:$/,"");s=$0;f=1;next}; NF&&f{ print s"/"$0 }'))
num_files=${#file_matrix[*]}
# This is the command you want to run on a random file.
# Change "ls -l" by anything you want, it's just an example.
ls -l "${file_matrix[$((RANDOM%num_files))]}"
fi
exit 0
MacOS no tiene los comandos sort -R y shuf , por lo que necesitaba una solución bash only que aleatorice todos los archivos sin duplicados y no lo encontré aquí. Esta solución es similar a la solución # 4 de gniourf_gniourf, pero con suerte agrega mejores comentarios.
El script debe ser fácil de modificar para detener después de N muestras usando un contador con if, o gniourf_gniourf's for loop with N. $ RANDOM está limitado a ~ 32000 archivos, pero eso debería ser para la mayoría de los casos.
#!/bin/bash
array=(*) # this is the array of files to shuffle
# echo ${array[@]}
for dummy in "${array[@]}"; do # do loop length(array) times; once for each file
length=${#array[@]}
randomi=$(( $RANDOM % $length )) # select a random index
filename=${array[$randomi]}
echo "Processing: '$filename'" # do something with the file
unset -v "array[$randomi]" # set the element at index $randomi to NULL
array=("${array[@]}") # remove NULL elements introduced by unset; copy array
done
Yo uso esto: usa un archivo temporal pero va profundamente en un directorio hasta que encuentra un archivo normal y lo devuelve.
# find for a quasi-random file in a directory tree:
# directory to start search from:
ROOT="/";
tmp=/tmp/mytempfile
TARGET="$ROOT"
FILE="";
n=
r=
while [ -e "$TARGET" ]; do
TARGET="$(readlink -f "${TARGET}/$FILE")" ;
if [ -d "$TARGET" ]; then
ls -1 "$TARGET" 2> /dev/null > $tmp || break;
n=$(cat $tmp | wc -l);
if [ $n != 0 ]; then
FILE=$(shuf -n 1 $tmp)
# or if you dont have/want to use shuf:
# r=$(($RANDOM % $n)) ;
# FILE=$(tail -n +$(( $r + 1 )) $tmp | head -n 1);
fi ;
else
if [ -f "$TARGET" ] ; then
rm -f $tmp
echo $TARGET
break;
else
# is not a regular file, restart:
TARGET="$ROOT"
FILE=""
fi
fi
done;
¿Qué tal una solución de Perl ligeramente modificada por el Sr. Kang aquí:
¿Cómo puedo mezclar las líneas de un archivo de texto en la línea de comandos de Unix o en un script de shell?
$ ls | perl -MList :: Util = shuffle -e '@lines = shuffle (<>); print @lines [0..4] '