for f in $(find .)
объединяет две несовместимые вещи.
find
печатает список путей к файлам, разделенных символами новой строки. Оператор split + glob, который вызывается, когда вы оставляете этот $ (find.)
без кавычек в контексте этого списка, разбивает его на символы $ IFS
(по умолчанию включает новую строку, но также пробел и табуляция (и NUL в zsh
)) и выполняет подстановку для каждого результирующего слова (кроме zsh
) (и даже раскрытие фигурных скобок в ksh93 (даже если фигурная скобка раскрывается
опция отключена в старых версиях) или производных pdksh!).
Даже если вы это сделаете:
IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion
# done upon other expansions in ksh)
for f in $(find .) # invoke split+glob
Это все равно неверно, поскольку символ новой строки так же допустим, как и любой другой в пути к файлу. Вывод find -print
просто не поддается постобработке (за исключением использования некоторого запутанного трюка , как показано здесь ).
Это также означает, что оболочке необходимо полностью сохранить вывод find
, а затем разделить + glob его (что подразумевает сохранение этого вывода во второй раз в памяти) перед тем, как начать цикл по файлам.
Обратите внимание, что находят.| xargs cmd
имеет аналогичные проблемы (там пробелы, новая строка, одинарные кавычки, двойные кавычки и обратная косая черта (а с некоторыми xarg
реализациями байты, не являющиеся частью допустимых символов) являются проблемой)
Единственный способ использовать цикл for
на выходе find
- это использовать zsh
, который поддерживает IFS = $ ' \ 0 '
и:
IFS=$'\0'
for f in $(find . -print0)
(замените -print0
на -exec printf'% s \ 0 '{} +
для найдите
реализации, которые не поддерживают нестандартный (но довольно распространенный в настоящее время) -print0
).
Здесь правильный и переносимый способ - использовать -exec
:
find . -exec something with {} \;
Или, если что-то
может принимать более одного аргумента:
find . -exec something with {} +
Если вам действительно нужен этот список файлов, которые будут обрабатываться оболочкой:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(будьте осторожны, она может запускать более одного sh
).
В некоторых системах вы можете использовать:
find . -print0 | xargs -r0 something with
, хотя это имеет небольшое преимущество перед стандартным синтаксисом и означает, что что-то
stdin
является либо каналом, либо / dev / null
.
Одна из причин, по которой вы можете захотеть использовать это, может заключаться в использовании опции -P
в GNU xargs
для параллельной обработки. Проблема stdin
также может быть решена с помощью GNU xargs
с параметром -a
с оболочками, поддерживающими замену процесса: например
xargs -r0n 20 -P 4 -a <(find . -print0) something
, чтобы запустить до 4 одновременных вызова чего-то
, каждый из которых принимает 20 аргументов файла.
С помощью zsh
или bash
, другой способ перебрать вывод find -print0
- это:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d ''
читает записи с разделителями NUL вместо записей с разделителями новой строки.
bash-4.4
и более поздние версии также могут хранить файлы, возвращенные командой find -print0
, в массиве с:
readarray -td '' files < <(find . -print0)
эквивалентом zsh
(который имеет преимущество сохранения статус выхода find
):
files=(${(0)"$(find . -print0)"})
С помощью zsh
вы можете преобразовать большинство выражений find
в комбинацию рекурсивной подстановки с квалификаторами glob. Например, перебирая find. -name '* .txt' -type f -mtime -1
будет:
for file (./**/*.txt(ND.m-1)) cmd $file
или
for file (**/*.txt(ND.m-1)) cmd -- $file
(остерегайтесь необходимости -
как с ** / *
, пути к файлам не начинаются с ./
, поэтому могут начинаться, например, с -
).
ksh93
и bash
в конечном итоге добавили поддержку ** /
(хотя и не более продвинутые формы рекурсивного подстановки), но все же не квалификаторы glob, которые используют **
там очень ограничено. Также помните, что bash
до 4.3 следует за символическими ссылками при спуске по дереву каталогов.
Как и в случае перебора $ (find.)
, это также означает сохранение всего списка файлов в памяти 1 .Это может быть желательно в некоторых случаях, когда вы не хотите, чтобы ваши действия с файлами влияли на поиск файлов (например, когда вы добавляете больше файлов, которые в конечном итоге могут быть найдены сами) .
Теперь, если мы говорим о надежности,мы должны упомянуть условия гонки между временем, когда find
/ zsh
находит файл и проверяет его соответствие критериям, а также время его использования ( TOCTOU race ]).
Даже при спуске по дереву каталогов необходимо следить за тем, чтобы не следовать символическим ссылкам и делать это без гонки TOCTOU. find
(как минимум GNU find
) делает это, открывая каталоги с помощью openat ()
с правильными флагами O_NOFOLLOW
(где поддерживается) и сохраняя дескриптор файла открытым для каждого каталога, zsh
/ bash
/ ksh
этого не делают. Таким образом, если злоумышленник сможет заменить каталог символической ссылкой в нужное время, вы можете в конечном итоге перейти не в тот каталог.
Даже если find
действительно спускается по каталогу правильно, с помощью -exec cmd {} \;
и тем более с -exec cmd {} +
, после выполнения cmd
, например, как cmd ./foo/bar
или cmd ./foo/bar ./foo/bar/baz
, к моменту времени cmd
использует ./ foo / bar
, атрибуты bar
могут больше не соответствовать критериям, указанным в find
, но даже хуже того, ./ foo
мог быть заменен символической ссылкой на другое место (а окно гонки стало намного больше с помощью -exec {} +
где find
ожидает достаточного количества файлов для вызова cmd
).
Некоторые реализации find
имеют (пока нестандартный) предикат -execdir
для решения второй проблемы.
С помощью:
find . -execdir cmd -- {} \;
найдите
chdir ()
s в родительском каталоге файла перед запуском cmd
. Вместо вызова cmd - ./foo/bar
он вызывает cmd - ./bar
( cmd - bar
с некоторыми реализациями, отсюда -
), поэтому проблема с изменением ./ foo
на символическую ссылку устранена. Это делает использование таких команд, как rm
, более безопасным (он все равно может удалить другой файл, но не файл в другом каталоге), но не команд, которые могут изменять файлы, если они не были разработаны так, чтобы не следовать символическим ссылкам .
-execdir cmd - {} +
иногда также работает, но с несколькими реализациями, включая некоторые версии GNU find
, он эквивалентен -execdir cmd - {} \;
.
-execdir
также позволяет обойти некоторые проблемы, связанные со слишком глубокими деревьями каталогов.
В:
find . -exec cmd {} \;
размер пути, заданного для cmd
, будет расти вместе с глубиной каталога, в котором находится файл. Если этот размер станет больше, чем PATH_MAX
(что-то например, 4k в Linux), то любой системный вызов, выполняемый cmd
по этому пути, завершится ошибкой ENAMETOOLONG
.
С -execdir
только имя файла (возможно, с префиксом ./
) передается в cmd
.Сами имена файлов в большинстве файловых систем имеют гораздо более низкий предел ( NAME_MAX
), чем PATH_MAX
, поэтому ошибка ENAMETOOLONG
встречается с меньшей вероятностью.
Кроме того, при рассмотрении вопросов безопасности, связанных с find
и, в более общем плане, с обработкой имен файлов в целом часто упускается из виду тот факт, что в большинстве Unix-подобных систем имена файлов представляют собой последовательности байтов. (любое байтовое значение, кроме 0 в пути к файлу, и в большинстве систем (основанных на ASCII, мы пока проигнорируем редкие, основанные на EBCDIC) 0x2f - разделитель пути).
Приложения должны решить, хотят ли они рассматривать эти байты как текст. И они обычно это делают, но обычно перевод байтов в символы выполняется на основе локали пользователя, на основе среды.
Это означает, что данное имя файла может иметь различное текстовое представление в зависимости от локали. Например, последовательность байтов 63 f4 74 e9 2e 74 78 74
будет côté.txt
для приложения, интерпретирующего это имя файла в локали с набором символов ISO-8859- 1 и cєtщ.txt
в локали, где вместо этого используется кодировка IS0-8859-5.
Хуже. В языковом стандарте, где используется кодировка UTF-8 (норма в настоящее время), 63 f4 74 e9 2e 74 78 74 просто невозможно сопоставить с символами!
find
- одно из таких приложений, которое рассматривает имена файлов как текст для своих предикатов -name
/ -path
(и других, например -iname
] или -regex
с некоторыми реализациями).
Это означает, что, например, с несколькими реализациями find
(включая GNU find
).
find . -name '*.txt'
не сможет найти наш 63 f4 74 e9 2e 74 78 74
файл выше при вызове в локали UTF-8 как *
(который соответствует 0 или более символов , а не байты) не может соответствовать этим несимволам.
LC_ALL = C find ...
может обойти проблему, поскольку локаль C подразумевает один байт на символ и (обычно) гарантирует, что все значения байтов отображаются на символ (хотя, возможно, неопределенные для некоторых значений байтов) .
Теперь, когда дело доходит до перебора этих имен файлов из оболочки, этот байт против символа также может стать проблемой. В этом отношении мы обычно видим 4 основных типа оболочек:
Те, которые еще не поддерживают многобайтовую обработку, такие как тире
. Для них байт соответствует символу. Например, в UTF-8 côté
- это 4 символа, но 6 байтов. В языковом стандарте, где кодировка UTF-8, в
найти. -имя '????' -exec тире -c '
имя = $ {1 ## * /}; echo "$ {# name}" 'sh {} \;
find
успешно найдет файлы, имя которых состоит из 4 символов в кодировке UTF-8, но тире
] будет указывать длину от 4 до 24.
yash
: наоборот. Он работает только с символами . Все вводимые данные внутренне переводятся в символы. Это делает оболочку наиболее согласованной, но это также означает, что она не может справиться с произвольными последовательностями байтов (теми, которые не преобразуются в допустимые символы). Даже в языковом стандарте C он не справляется со значениями байтов выше 0x7f.
находят.-exec yash -c 'echo "$ 1"' sh {} \;
в языковом стандарте UTF-8 не будет работать с нашим ISO-8859-1 côté.txt
из более ранней версии. например.
Такие, как bash
или zsh
, где постепенно добавлялась поддержка многобайтовой информации. Они вернутся к рассмотрению байтов, которые нельзя сопоставить с символами, как если бы они были символами. У них все еще есть несколько ошибок здесь и там, особенно с менее распространенными многобайтовыми кодировками, такими как GBK или BIG5-HKSCS (они довольно неприятны, поскольку многие из их многобайтовых символов содержат байты в диапазоне 0-127 (например, символы ASCII) ).
Такие, как sh
FreeBSD (по крайней мере, 11) или mksh -o utf8-mode
, которые поддерживают многобайтовый формат, но только для UTF-8.
1 Для полноты мы могли бы упомянуть хакерский способ в zsh
перебирать файлы с помощью рекурсивного подстановки без сохранения всего списка в памяти:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+ cmd
] - квалификатор glob, который вызывает cmd
(обычно функция) с текущим путем к файлу в $ REPLY
. Функция возвращает истину или ложь, чтобы решить, следует ли выбрать файл (а также может изменить $ REPLY
или вернуть несколько файлов в массиве $ reply
). Здесь мы выполняем обработку в этой функции и возвращаем false, чтобы файл не был выбран.
while true; do
echo 'Looping, press Ctrl+C to exit'
sleep 5
done
Нет нужды усложнять это.
Следующее требуетbash
:
while true; do
echo 'Press any key to exit, or wait 5 seconds'
if read -r -N 1 -t 5; then
break
fi
done
Если read
выходит из строя (по тайм-ауту ), цикл продолжается.
Следует иметь в виду, что оболочки или любые приложения, которые вы обычно запускаете в терминале, не взаимодействуют с клавиатурой и экраном.
Они принимают ввод со своего стандартного ввода в виде потока байтов. Когда этот stdin приходит из обычного файла, байты поступают оттуда, когда это канал, это данные, которые обычно отправляются другим процессом, когда это какой-то файл устройства, который может поступать на физические устройства, подключенные к компьютеру. Например, когда это символьное устройство tty, это данные, отправляемые по некоторой последовательной линии, как правило, терминалом. Терминал — это устройство, которое преобразует события клавиатуры в последовательности байтов.
Вот в чем заключается вся мощь терминальных приложений. Механизм ввода для них абстрагирован, поэтому их можно использовать интерактивно или автоматически в сценариях.
Здесь, если вы собираетесь выдавать такое приглашение и ожидаете событие нажатия клавиши , вы, вероятно, захотите, чтобы ваше приложение (ваш скрипт )был только интерактивным. Либо ожидайте, что стандартный ввод будет терминалом, либо примите ввод с терминала независимо от того, на чем открыт стандартный ввод.
Теперь, как показано выше, все приложения видят потоки байтов, это терминал (или эмулятор терминала )и роль дисциплины линии устройства tty для преобразования нажатой клавиши в последовательности байтов. Несколько примеров:
xterm
в системах на основе ASCII -отправит либо 0x1b 0x4f 0x44, либо 0x1b 0x5b 0x44(<ESC>[A
или<ESC>OA
). Итак, вот вопросы, которые я бы задал:
Здесь я предполагаю, что вы хотите, чтобы ваш сценарий был только интерактивным приложением терминала и взаимодействовал только через управляющий терминал, оставляя стандартный ввод/стандартный вывод в покое.
#! /bin/sh -
# ":" being a special builtin, POSIX requires it to exit if a
# redirection fails, which makes this a way to easily check if a
# controlling terminal is present and readable:
:</dev/tty
# if using bash however not in POSIX conformance mode, you'll need to
# change it to something like:
exec 3< /dev/tty 3<&- || exit
read_key_with_timeout() (
timeout=$1 prompt=$2
saved_tty_settings=$(stty -g) || exit
# if we're killed, restore the tty settings, the convoluted part about
# killing the subshell process is to work around a problem in shells
# like bash that ignore a SIGINT if the current command being run handles
# it.
for sig in INT TERM QUIT; do
trap '
stty "$saved_tty_settings"
trap - '"$sig"'
pid=$(exec sh -c '\''echo "$PPID"'\'')
kill -s '"$sig"' "$pid"
# fall back if kill failed above
exit 2' "$sig"
done
# drain the tty's buffer
stty -icanon min 0 time 0; cat > /dev/null
printf '%s\n' "$prompt"
# use the tty line discipline features to say the next read()
# should wait at most the given number of deciseconds (limited to 255)
stty time "$((timeout * 10))" -echo
# do one read and count the bytes returned
count=$(dd 2> /dev/null count=1 | wc -c)
# If the user pressed a key like the £ or Home ones described above
# it's likely all the corresponding bytes will have been read by dd
# above, but not guaranteed, so we may want to drain the tty buffer
# again to make sure we don't leave part of the sequence sent by a
# key press to be read by the next thing that reads from the tty device
# thereafter. Here allowing the terminal to send bytes as slow as 10
# per second. Doing so however, we may end up reading the bytes sent
# upon subsequent key presses though.
stty time 1; cat > /dev/null
stty "$saved_tty_settings"
# return whether at least one byte was read:
[ "$(($count))" -gt 0 ]
) <> /dev/tty >&0 2>&0
until
echo "Hello World"
sleep 1
echo "Done greeting the world"
read_key_with_timeout 5 "Press any key to stop"
do
continue
done