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

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

Вот мое решение в 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.


person JasonSmith    schedule 31.03.2009    source источник
comment
В способе bash вместо ... будет использоваться $ (...).   -  person ashawley    schedule 31.03.2009
comment
Хорошая точка зрения. Мне было немного непонятно. На практике я использую Bash в Linux, но теоретически было бы здорово, если бы он работал на sh в Unix, что означает обратные кавычки и отсутствие coreutils GNU.   -  person JasonSmith    schedule 31.03.2009
comment
@JasonSmith $(…) находится в POSIX. Если у вас все еще есть оболочка, которая ее не поддерживает, поместите /usr/xpg4/bin или что-то подобное перед /usr/bin на вашем PATH и вызовите /usr/bin/env sh, а не /bin/sh. (Или у вас настоящий антиквариат.)   -  person Gilles 'SO- stop being evil'    schedule 19.07.2011


Ответы (11)


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

И не разбирайте ls. Прочтите http://mywiki.wooledge.org/ParsingLs

Изменить: удачи в поиске надежного решения, отличного от bash. Большинство из них будет нарушено для определенных типов имен файлов, таких как имена файлов с пробелами, новой строкой или дефисом (это практически невозможно в чистом sh). Чтобы сделать это правильно без bash, вам нужно будет полностью перейти на _5 _ / _ 6 _ / _ 7 _ / ... без передачи этого вывода для дальнейшей обработки или чего-то подобного.

person lhunath    schedule 31.03.2009
comment
RANDOM и массивы являются функциями Bash, и OP заинтересован в более переносимой (не GNU) версии для использования в собственно Unix. - person ashawley; 31.03.2009
comment
Спасибо @lhunath, тезис о ls хорошо усвоен. Обновил вопрос. - person JasonSmith; 31.03.2009
comment
ваш пример на самом деле не работает, printf "%s\n" "${files[RANDOM % ${#files}]}" должен быть printf "%s\n" "${files[RANDOM % ${#files[@]}]}" - ${#files} представляет длину (strlen) первого значения в массиве files. ${#files[@]} представляет количество элементов в массиве files, что нам и нужно. - person sente; 08.02.2011
comment
Обработать произвольные имена файлов в портативном sh не намного сложнее, чем в bash. Единственное, что в bash упрощает, - это массивы, и это полезно только тогда, когда вам нужно одновременно манипулировать несколькими списками имен файлов. - person Gilles 'SO- stop being evil'; 19.07.2011
comment
Обратите внимание, что printf не является частью решения, если вы не хотите, чтобы имя файла было на стандартном выводе, а не в качестве аргумента для произвольной команды. - person Peter Cordes; 18.01.2017
comment
Если я правильно понял вашу ссылку, синтаксический анализ ls является проблемой только в том случае, если в них есть файлы, содержащие символы новой строки. Во многих случаях люди могут точно знать, что файлы в каталоге, который они разбирают, не будут содержать ни одного из таких файлов. Синтаксический анализ ls особенно прост, если вы хотите делать что-то прямо в командной строке, а не писать скрипт. - person Rapti; 03.11.2017
comment
@Rapti, нет, синтаксический анализ ls всегда сложнее, чем не анализировать ls. $(ls) намного сложнее, чем *, и вызывает ошибки. Нет оправдания синтаксическому разбору ls. ls - это инструмент для людей, а не для кода. Каждый раз, когда вы разбираете ls, вы добавляете возможность для ошибок и в то же время усложняете себе жизнь. Все, что вам нужно сделать, это узнать, что такое шары, и ls вам больше никогда не понадобится. - person lhunath; 04.11.2017

"Шуф" не переносится?

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

или найдите, если файлы находятся глубже одного каталога:

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

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

person johnnyB    schedule 02.04.2013
comment
Действительно полезно для людей, которым нужно просто работать. Неважно, кто, неважно, насколько он хакерский. - person Allison; 28.04.2014
comment
Вы можете использовать gshuf (brew install gshuf) на Mac. Точно работает с Mavericks, но не тестировался ни на каких других версиях! - person Matt Fletcher; 28.10.2014
comment
shuf теперь находится в формуле coreutils с префиксом g (введите gshuf после установки формулы coreutils) - person Frizlab; 24.11.2014
comment
brew install gshuf у меня не сработало, но brew install coreutils сработало. - person JW.; 11.02.2015

Что-то типа:

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

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

person fido    schedule 31.03.2009
comment
Плакат хочет решение без башизмов. - person ashawley; 31.03.2009
comment
@MGoDave, не так уж плохо. Меня всегда интересует хорошее решение для Bash и хорошее решение без GNU для различных ситуаций и в качестве умственного упражнения. - person JasonSmith; 01.04.2009
comment
И что именно # файл? - person harperville; 23.07.2013
comment
@harperville ${#file} - количество элементов в массиве bash file - person hoijui; 22.11.2018

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

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

person Aaron Digulla    schedule 31.03.2009
comment
Неплохо подмечено. ls -1 является стандартом для Unix или это просто GNU? В любом случае, да, самая большая проблема - получить случайное число. Я бы сказал, что Perl довольно универсален, поскольку он стал стандартом со времен IIRC Solaris 2.6 и HP-UX 11i. - person JasonSmith; 31.03.2009
comment
-1 в качестве аргумента для ls является стандартным в SUS2 (opengroup.org/onlinepubs/007908799/ xcu / ls.html). Я не знаю, когда он был добавлен, но я считаю, что он был доступен и во времена POSIX. - person Chas. Owens; 31.03.2009
comment
@Chas спасибо за ссылку. Тем не менее, Аарон считает, что имена файлов с новой строкой могут вызвать проблемы. Так что это может быть актуально в зависимости от того, позволяете ли вы гражданским лицам создавать файлы непосредственно в файловой системе и каким образом. - person JasonSmith; 01.04.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"

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

person ashawley    schedule 31.03.2009
comment
Это отличная идея. Вам нужно просканировать каталог дважды, и если количество файлов изменяется между сканированиями, возникает состояние гонки, но на практике это, вероятно, не имеет большого значения. - person JasonSmith; 01.04.2009
comment
Да, я убежден, что традиционное программирование оболочки Bourne в корне ошибочно для многих ситуаций, независимо от того, как вы прилагаете все усилия. Введите Bash и GNU coreutils, чтобы спасти положение. - person ashawley; 01.04.2009
comment
Awk действительно дает вам случайное число, и это единственный способ, предлагаемый POSIX, но это очень плохой RNG (предсказуемый, и вывод изменяется только один раз в секунду). Кроме того, не анализирует вывод ls. - person Gilles 'SO- stop being evil'; 19.07.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
person gsbabil    schedule 26.06.2011

Вот фрагмент оболочки, который полагается только на функции POSIX и справляется с произвольными именами файлов (но исключает точечные файлы из выбора). При случайном выборе используется awk, потому что это все, что вы получаете в POSIX. Это очень плохой генератор случайных чисел, поскольку ГСЧ 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 % $#))
person Gilles 'SO- stop being evil'    schedule 19.07.2011

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-; это необходимо, чтобы избежать усечения файлов, содержащих пробелы (или любой другой разделитель, который вы хотите использовать).

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

person Robert Calhoun    schedule 08.04.2015

Поместите каждую строку вывода команды 'ls' в ассоциативный массив с именем line, а затем выберите один из таких ...

ls | awk '{ line[NR]=$0 } END { print line[(int(rand()*NR+1))]}'
person kapu    schedule 16.02.2016
comment
Первый набор фигурных скобок {line [NR] = $ 0} создает ассоциативный массив с произвольным именем 'line', в котором хранится каждая строка вывода из ls, проиндексированная NR, которая является специальной переменной awk, которая указывает номер записи. . После того, как все строки вывода были сохранены в массиве, awk переходит в раздел END. NR в этой точке равно общему количеству строк вывода от ls. Итак, мы выбираем случайное число из NR и извлекаем строку по этому индексу. Чтобы лучше ответить на вопрос OP, можно заменить ls на find. -maxdepth 1 -type f ' - person kapu; 18.02.2016

Мои 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"
person Jay jargot    schedule 23.11.2018

person    schedule
comment
Было бы неплохо опубликовать небольшое объяснение вместе с кодом. - person BBog; 06.11.2012