Ho una directory con circa 2000 file. Come posso selezionare un campione casuale di N
file usando uno script bash o un elenco di comandi inoltrati?
ls | shuf -n 5
Fonte da Unix Stackexchange
Ho una directory con circa 2000 file. Come posso selezionare un campione casuale di N
file usando uno script bash o un elenco di comandi inoltrati?
ls | shuf -n 5
Fonte da Unix Stackexchange
Risposte:
Ecco uno script che utilizza l'opzione casuale dell'ordinamento GNU:
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"
, non mostrato, sarebbe sensibile agli spazi.
Puoi usare shuf
(dal pacchetto GNU coreutils) per questo. Basta inserire un elenco di nomi di file e chiedergli di restituire la prima riga da una permutazione casuale:
ls dirname | shuf -n 1
# probably faster and more flexible:
find dirname -type f | shuf -n 1
# etc..
Regola il -n, --head-count=COUNT
valore per restituire il numero di righe desiderate. Ad esempio, per restituire 5 nomi di file casuali dovresti usare:
find dirname -type f | shuf -n 5
N
file casuali, quindi l'utilizzo 1
è un po 'fuorviante.
find dirname -type f -print0 | shuf -zn1
Ecco alcune possibilità che non analizzano l'output di ls
e che sono sicure al 100% per quanto riguarda i file con spazi e simboli divertenti nel loro nome. Tutti loro popoleranno un array randf
con un elenco di file casuali. Questo array può essere facilmente stampato con printf '%s\n' "${randf[@]}"
se necessario.
Questo probabilmente produrrà lo stesso file più volte e N
deve essere conosciuto in anticipo. Qui ho scelto N = 42.
a=( * )
randf=( "${a[RANDOM%${#a[@]}]"{1..42}"}" )
Questa funzione non è ben documentata.
Se N non è noto in anticipo, ma ti è davvero piaciuta la possibilità precedente, puoi usarla eval
. Ma è malvagio e devi davvero assicurarti che N
non provenga direttamente dall'input dell'utente senza essere accuratamente controllato!
N=42
a=( * )
eval randf=( \"\${a[RANDOM%\${#a[@]}]\"\{1..$N\}\"}\" )
Personalmente non mi piace eval
e quindi questa risposta!
Lo stesso usando un metodo più semplice (un ciclo):
N=42
a=( * )
randf=()
for((i=0;i<N;++i)); do
randf+=( "${a[RANDOM%${#a[@]}]}" )
done
Se non vuoi avere più volte lo stesso file:
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 . Questa è una risposta tardiva a un vecchio post, ma la risposta accettata si collega a una pagina esterna che mostra terribilebashpratica, e l'altra risposta non è molto migliore in quanto analizza anche l'output di ls
. Un commento alla risposta accettata indica una risposta eccellente di Lhunath che ovviamente mostra buone pratiche, ma non risponde esattamente al PO.
"{1..42}"
parte lasciasse un finale "1"
. Inoltre, $RANDOM
è solo 15 bit e il metodo non funzionerà con oltre 32767 file tra cui scegliere.
ls | shuf -n 10 # ten random files
ls
. Questo non funzionerà se, ad esempio, un nome file contiene nuove righe.
ls
non è garantito che ti dia nomi di file "puliti" quindi non dovresti fare affidamento su di esso, punto. Il fatto che questi problemi siano rari o insoliti non cambia il problema; soprattutto dato che ci sono soluzioni migliori per questo.
ls
può includere directory e righe vuote. Suggerirei qualcosa di simile find . -type f | shuf -n10
invece.
Una soluzione semplice per selezionare 5
file casuali evitando di analizzare ls . Funziona anche con file contenenti spazi, newline e altri caratteri speciali:
shuf -ezn 5 * | xargs -0 -n1 echo
Sostituisci echo
con il comando che desideri eseguire per i tuoi file.
read
ha gli stessi problemi dell'analisi ls
? vale a dire, legge riga per riga, quindi non funziona per i file con nuove righe nel loro nome
Se hai installato Python (funziona con Python 2 o Python 3):
Per selezionare un file (o una riga da un comando arbitrario), utilizzare
ls -1 | python -c "import sys; import random; print(random.choice(sys.stdin.readlines()).rstrip())"
Per selezionare N
file / linee, utilizzare (la nota N
è alla fine del comando, sostituirla con un numero)
ls -1 | python -c "import sys; import random; print(''.join(random.sample(sys.stdin.readlines(), int(sys.argv[1]))).rstrip())" N
Questa è una risposta anche più tardi alla risposta tardiva di @ gniourf_gniourf, che ho appena votato perché è di gran lunga la risposta migliore, due volte. (Una volta per evitare eval
e una volta per una gestione sicura del nome file.)
Ma mi ci sono voluti alcuni minuti per districare le funzioni "non molto ben documentate" utilizzate da questa risposta. Se le tue abilità di Bash sono abbastanza solide da vedere immediatamente come funziona, salta questo commento. Ma non l'ho fatto e, dopo averlo districato, penso che valga la pena spiegarlo.
La funzione n. 1 è il globbing del file della shell. a=(*)
crea un array, i $a
cui membri sono i file nella directory corrente. Bash comprende tutte le stranezze dei nomi dei file, quindi l'elenco è garantito in modo corretto, è garantito il escape, ecc. Non è necessario preoccuparsi di analizzare correttamente i nomi di file testuali restituiti da ls
.
La funzione # 2 è espansioni di parametri Bash per array , uno nidificato all'interno di un altro. Questo inizia con ${#ARRAY[@]}
, che si espande fino alla lunghezza di $ARRAY
.
Tale espansione viene quindi utilizzata per sottoscrivere l'array. Il modo standard per trovare un numero casuale compreso tra 1 e N è prendere il valore di un numero casuale modulo N. Vogliamo un numero casuale compreso tra 0 e la lunghezza del nostro array. Ecco l'approccio, suddiviso in due righe per motivi di chiarezza:
LENGTH=${#ARRAY[@]}
RANDOM=${a[RANDOM%$LENGTH]}
Ma questa soluzione lo fa in una sola riga, rimuovendo l'assegnazione variabile non necessaria.
La funzione # 3 è l' espansione di Bash Brace , anche se devo confessare che non lo capisco del tutto. Espansione delle parentesi graffe viene utilizzato, ad esempio, per generare un elenco di 25 file di nome filename1.txt
, filename2.txt
ecc: echo "filename"{1..25}".txt"
.
L'espressione all'interno della subshell sopra, "${a[RANDOM%${#a[@]}]"{1..42}"}"
usa quel trucco per produrre 42 espansioni separate. L'espansione del controvento posiziona una singola cifra tra il ]
e il }
, che all'inizio pensavo stesse sottoscrivendo l'array, ma in tal caso sarebbe preceduto da due punti. (Avrebbe anche restituito 42 oggetti consecutivi da un punto casuale dell'array, il che non è affatto la stessa cosa di restituire 42 oggetti casuali dall'array.) Penso che stia solo facendo eseguire l'espansione della shell 42 volte, restituendo così 42 oggetti casuali dall'array. (Ma se qualcuno può spiegarlo in modo più completo, mi piacerebbe ascoltarlo.)
Il motivo per cui N deve essere hardcoded (a 42) è che l'espansione del controvento avviene prima dell'espansione variabile.
Infine, ecco la funzione # 4 , se vuoi farlo in modo ricorsivo per una gerarchia di directory:
shopt -s globstar
a=( ** )
Questo attiva un'opzione shell che fa **
corrispondere ricorsivamente. Ora il tuo $a
array contiene tutti i file nell'intera gerarchia.
Se nella cartella sono presenti più file, è possibile utilizzare il comando di piping riportato di seguito che ho trovato in unix stackexchange .
find /some/dir/ -type f -print0 | xargs -0 shuf -e -n 8 -z | xargs -0 cp -vt /target/dir/
Qui volevo copiare i file, ma se si desidera spostare i file o fare qualcos'altro, basta cambiare l'ultimo comando dove ho usato cp
.
Questo è l'unico script che riesco a interpretare bene con bash su MacOS. Ho combinato e modificato frammenti dai seguenti due collegamenti:
Comando ls: come posso ottenere un elenco ricorsivo a percorso completo, una riga per file?
#!/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 non ha i comandi sort -R e shuf , quindi avevo bisogno di una soluzione solo bash che randomizza tutti i file senza duplicati e non li ho trovati qui. Questa soluzione è simile alla soluzione n. 4 di gniourf_gniourf, ma si spera che aggiunga commenti migliori.
Lo script dovrebbe essere facile da modificare per interrompere dopo N campioni usando un contatore con if, oppure gniourf_gniourf per il ciclo con N. $ RANDOM è limitato a ~ 32000 file, ma dovrebbe succedere nella maggior parte dei casi.
#!/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
Lo uso: usa un file temporaneo ma entra profondamente in una directory fino a quando non trova un file normale e lo restituisce.
# 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;
Che ne dite di una soluzione Perl leggermente modificata da Mr. Kang qui:
Come posso mescolare le righe di un file di testo sulla riga di comando di Unix o in uno script di shell?
$ ls | perl -MList :: Util = shuffle -e '@lines = shuffle (<>); print @lines [0..4] '