Лучший способ выбрать случайный файл из каталога в сценарии оболочки - PullRequest
44 голосов
/ 31 марта 2009

Каков наилучший способ выбрать случайный файл из каталога в сценарии оболочки?

Вот мое решение на Bash, но я был бы очень заинтересован в более переносимой (не GNU) версии для использования на собственно 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"

У кого-нибудь есть еще идеи?

Редактировать: lhunath хорошо подходит для анализа ls. Я думаю, все сводится к тому, хотите ли вы быть портативным или нет. Если у вас есть GNU findutils и coreutils, вы можете сделать:

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

Уфф, это было весело! Также это лучше соответствует моему вопросу, так как я сказал «случайный файл». Честно говоря, в наши дни трудно представить систему Unix, развернутую там с установленным GNU, но не с Perl 5.

Ответы [ 11 ]

58 голосов
/ 31 марта 2009
files=(/my/dir/*)
printf "%s\n" "${files[RANDOM % ${#files[@]}]}"

И не разбирать ls . Читать http://mywiki.wooledge.org/ParsingLs

Редактировать: Удачи в поиске надежного решения, отличного от bash. Большинство будет ломаться для определенных типов имен файлов, таких как имена файлов с пробелами или символами новой строки или тире (в чистом sh это практически невозможно). Чтобы сделать это правильно без bash, вам необходимо полностью перейти на awk / perl / python / ... без передачи этого вывода для дальнейшей обработки или тому подобного.

27 голосов
/ 02 апреля 2013

«Шуф» не переносимый?

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

или найдите файлы глубже, чем один каталог:

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

это часть coreutils, но вам понадобится 6.4 или новее, чтобы получить его ... поэтому RH / CentOS его не включает.

3 голосов
/ 06 ноября 2012
# ******************************************************************
# ******************************************************************
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 голосов
/ 31 марта 2009

Что-то вроде:

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

$RANDOM в bash - это специальная переменная, которая возвращает случайное число, затем я использую деление по модулю для получения действительного индекса, а затем ссылаюсь на этот индекс в массиве.

2 голосов
/ 19 июля 2011

Вот фрагмент оболочки, который опирается только на функции POSIX и справляется с произвольными именами файлов (но в выборке пропускает точечные файлы). Случайный выбор использует awk, потому что это все, что вы получаете в POSIX. Это очень плохой генератор случайных чисел, так как RNG в awk засеивается с текущим временем в секундах (поэтому он легко предсказуем и возвращает тот же выбор, если вы вызываете его несколько раз в секунду).

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

Если вы не хотите игнорировать точечные файлы, код генерации имени файла (set -- *) необходимо заменить чем-то более сложным.

set -- *; [ -e "$1" ] || shift
set .[!.]* "$@"; [ -e "$1" ] || shift
set ..?* "$@"; [ -e "$1" ] || shift
if [ $# -eq 0]; then echo 1>&2 "empty directory"; exit 1; fi

Если у вас есть OpenSSL, вы можете использовать его для генерации случайных байтов. Если у вас нет, но в вашей системе установлено значение /dev/urandom, замените вызов на openssl на dd if=/dev/urandom bs=3 count=1 2>/dev/null. Вот фрагмент, который устанавливает n в случайное значение между 1 и $#, стараясь не вносить смещение. В этом фрагменте предполагается, что $# не более 2 ^ 23-1.

while
  n=$(($(openssl rand 3 | od -An -t u4) + 1))
  [ $n -gt $((16777216 / $# * $#)) ]
do :; done
n=$((n % $#))
2 голосов
/ 26 июня 2011

Новых строк в именах файлов можно избежать, выполнив в 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 голосов
/ 31 марта 2009

Я думаю, что Awk - хороший инструмент для получения случайного числа. Согласно Advanced Bash Guide , Awk является хорошей заменой случайного числа для $RANDOM.

Вот версия вашего скрипта, в которой не используются инструменты Bash-isms и 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"

Он наследует проблемы, упомянутые в других ответах, если файлы содержат символы новой строки.

2 голосов
/ 31 марта 2009

Это сводится к следующему: Как я могу создать случайное число в сценарии Unix переносимым способом?

Потому что, если у вас есть случайное число от 1 до N, вы можете использовать head -$N | tail, чтобы вырезать где-то посередине. К сожалению, я не знаю портативного способа сделать это с помощью одной оболочки. Если у вас есть Python или Perl, вы можете легко использовать их произвольную поддержку, но AFAIK, стандартной команды rand(1) нет.

1 голос
/ 08 апреля 2015

BusyBox (используется на встроенных устройствах) обычно настроен на поддержку $RANDOM, но не имеет массивов в стиле bash, sort --random-sort или shuf. Отсюда следующее:

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

Трейлинг ноты "-" в cut -f2-; это необходимо, чтобы избежать усечения файлов, содержащих пробелы (или любой разделитель, который вы хотите использовать).

Он не будет правильно обрабатывать имена файлов со встроенными символами новой строки.

0 голосов
/ 23 ноября 2018

Мои 2 цента с версией, которая не должна ломаться, когда существуют имена файлов со специальными символами:

#!/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"
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...