Eu tenho um diretório com cerca de 2000 arquivos. Como posso selecionar uma amostra aleatória de N
arquivos usando um script bash ou uma lista de comandos canalizados?
ls | shuf -n 5
Fonte: Unix Stackexchange
Eu tenho um diretório com cerca de 2000 arquivos. Como posso selecionar uma amostra aleatória de N
arquivos usando um script bash ou uma lista de comandos canalizados?
ls | shuf -n 5
Fonte: Unix Stackexchange
Respostas:
Aqui está um script que usa a opção aleatória do 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"
, não mostrado, seria sensível aos espaços.
Você pode usar shuf
(do pacote GNU coreutils) para isso. Apenas alimente uma lista de nomes de arquivos e peça para retornar a primeira linha de uma permutação aleatória:
ls dirname | shuf -n 1
# probably faster and more flexible:
find dirname -type f | shuf -n 1
# etc..
Ajuste o -n, --head-count=COUNT
valor para retornar o número de linhas desejadas. Por exemplo, para retornar 5 nomes de arquivos aleatórios, você usaria:
find dirname -type f | shuf -n 5
N
arquivos aleatórios, portanto, usar 1
é um pouco enganador.
find dirname -type f -print0 | shuf -zn1
Aqui estão algumas possibilidades que não analisam a saída ls
e são 100% seguras em relação a arquivos com espaços e símbolos engraçados em seu nome. Todos eles preencherão uma matriz randf
com uma lista de arquivos aleatórios. Essa matriz é facilmente impressa, printf '%s\n' "${randf[@]}"
se necessário.
Esse arquivo possivelmente produzirá o mesmo arquivo várias vezes e N
precisa ser conhecido com antecedência. Aqui eu escolhi N = 42.
a=( * )
randf=( "${a[RANDOM%${#a[@]}]"{1..42}"}" )
Esse recurso não está muito bem documentado.
Se N não for conhecido antecipadamente, mas você realmente gostou da possibilidade anterior, você pode usá-lo eval
. Mas é ruim, e você deve realmente garantir que N
isso não venha diretamente da entrada do usuário sem ser cuidadosamente verificado!
N=42
a=( * )
eval randf=( \"\${a[RANDOM%\${#a[@]}]\"\{1..$N\}\"}\" )
Eu pessoalmente não gosto eval
e, portanto, esta resposta!
O mesmo usando um método mais direto (um loop):
N=42
a=( * )
randf=()
for((i=0;i<N;++i)); do
randf+=( "${a[RANDOM%${#a[@]}]}" )
done
Se você não deseja ter várias vezes o mesmo arquivo:
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 . Essa é uma resposta tardia a uma postagem antiga, mas a resposta aceita está vinculada a uma página externa que mostra informações terríveis.festançaprática, e a outra resposta não é muito melhor, pois também analisa a saída de ls
. Um comentário à resposta aceita indica uma excelente resposta de Lhunath, que obviamente mostra boas práticas, mas não responde exatamente ao OP.
"{1..42}"
parte deixando um rastro "1"
. Além disso, $RANDOM
possui apenas 15 bits e o método não funcionará com mais de 32767 arquivos para você escolher.
ls | shuf -n 10 # ten random files
ls
. Isso não funcionará se, por exemplo, um nome de arquivo contiver novas linhas.
ls
não é garantido que você forneça nomes de arquivos "limpos", para que você não confie nele, ponto final. O fato de esses problemas serem raros ou incomuns não muda o problema; especialmente dado que existem melhores soluções para isso.
ls
pode incluir diretórios e linhas em branco. Eu sugeriria algo como isso find . -type f | shuf -n10
.
Uma solução simples para selecionar 5
arquivos aleatórios enquanto evita analisar ls . Também funciona com arquivos que contêm espaços, novas linhas e outros caracteres especiais:
shuf -ezn 5 * | xargs -0 -n1 echo
Substitua echo
pelo comando que você deseja executar para seus arquivos.
read
tem os mesmos problemas que a análise ls
? ou seja, ele lê linha por linha, por isso não funciona para arquivos com novas linhas em seu nome
Se você possui o Python instalado (funciona com o Python 2 ou o Python 3):
Para selecionar um arquivo (ou linha de um comando arbitrário), use
ls -1 | python -c "import sys; import random; print(random.choice(sys.stdin.readlines()).rstrip())"
Para selecionar N
arquivos / linhas, use (a nota N
está no final do comando, substitua-a por um número)
ls -1 | python -c "import sys; import random; print(''.join(random.sample(sys.stdin.readlines(), int(sys.argv[1]))).rstrip())" N
Esta é uma resposta ainda mais tarde à resposta tardia de @ gniourf_gniourf, que acabei de votar porque é de longe a melhor resposta, duas vezes. (Uma vez para evitar eval
e outra para manipulação segura de nome de arquivo.)
Mas levei alguns minutos para desembaraçar o (s) recurso (s) não muito bem documentado (s) que esta resposta usa. Se suas habilidades no Bash forem sólidas o suficiente para você ver imediatamente como isso funciona, pule este comentário. Mas não o fiz e, depois de desembaraçar, acho que vale a pena explicar.
O recurso 1 é o globbing do arquivo do próprio shell. a=(*)
cria uma matriz, $a
cujos membros são os arquivos no diretório atual. O Bash entende todas as estranhezas dos nomes de arquivos, para que a lista seja garantida correta, garantida como escapada, etc. Não é necessário se preocupar em analisar corretamente os nomes de arquivos de texto retornados por ls
.
O recurso 2 é expansões de parâmetro Bash para matrizes , uma aninhada dentro de outra. Isso começa com ${#ARRAY[@]}
, que se expande para o comprimento de $ARRAY
.
Essa expansão é então usada para subscrever a matriz. A maneira padrão de encontrar um número aleatório entre 1 e N é pegar o valor do módulo número aleatório N. Queremos um número aleatório entre 0 e o comprimento da nossa matriz. Aqui está a abordagem, dividida em duas linhas por uma questão de clareza:
LENGTH=${#ARRAY[@]}
RANDOM=${a[RANDOM%$LENGTH]}
Mas esta solução faz isso em uma única linha, removendo a atribuição desnecessária de variáveis.
O recurso 3 é a expansão do suporte Bash , embora eu tenha que confessar que não o entendo completamente. Expansão Brace é usado, por exemplo, para gerar uma lista de 25 arquivos nomeados filename1.txt
, filename2.txt
, etc:echo "filename"{1..25}".txt"
.
A expressão dentro do subshell acima,, "${a[RANDOM%${#a[@]}]"{1..42}"}"
usa esse truque para produzir 42 expansões separadas. A expansão de colchete coloca um único dígito entre o ]
e o }
, que no começo eu pensei que estava assinando o array, mas, se fosse, seria precedido por dois pontos. (Ele também retornaria 42 itens consecutivos de um ponto aleatório na matriz, o que não é o mesmo que devolver 42 itens aleatórios da matriz.) Acho que está apenas fazendo o shell executar a expansão 42 vezes, retornando assim 42 itens aleatórios da matriz. (Mas se alguém puder explicar melhor, eu adoraria ouvir.)
A razão pela qual N precisa ser codificado (a 42) é que a expansão do braquete ocorre antes da expansão variável.
Finalmente, aqui está o recurso nº 4 , se você quiser fazer isso recursivamente para uma hierarquia de diretórios:
shopt -s globstar
a=( ** )
Isso ativa uma opção de shell que faz **
corresponder recursivamente. Agora sua $a
matriz contém todos os arquivos em toda a hierarquia.
Se você tiver mais arquivos em sua pasta, poderá usar o comando canalizado abaixo que encontrei no unix stackexchange .
find /some/dir/ -type f -print0 | xargs -0 shuf -e -n 8 -z | xargs -0 cp -vt /target/dir/
Aqui eu queria copiar os arquivos, mas se você quiser mover arquivos ou fazer outra coisa, basta alterar o último comando em que eu usei cp
.
Este é o único script que eu consigo jogar bem com o bash no MacOS. Combinei e editei trechos dos dois links a seguir:
Comando ls: como posso obter uma listagem de caminho completo recursiva, uma linha por arquivo?
#!/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
O MacOS não possui os comandos sort -R e shuf , então eu precisava de uma solução somente bash que randomize todos os arquivos sem duplicatas e não encontrou aqui. Esta solução é semelhante à solução nº 4 de gniourf_gniourf, mas, com sorte, adiciona melhores comentários.
O script deve ser fácil de modificar para parar após N amostras usando um contador com if, ou o loop for de gniourf_gniourf com N. $ RANDOM é limitado a ~ 32000 arquivos, mas isso deve ocorrer na maioria dos 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
Eu uso isso: ele usa arquivo temporário, mas vai profundamente em um diretório até encontrar um arquivo regular e devolvê-lo.
# 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;
Que tal uma solução Perl levemente adulterada pelo Sr. Kang aqui:
Como posso embaralhar as linhas de um arquivo de texto na linha de comando do Unix ou em um script de shell?
$ ls | perl -MList :: Util = shuffle -e '@lines = shuffle (<>); print @lines [0..4] '