Нажмите любую клавишу, чтобы остановить цикл

Проблема
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 основных типа оболочек:

  1. Те, которые еще не поддерживают многобайтовую обработку, такие как тире . Для них байт соответствует символу. Например, в UTF-8 côté - это 4 символа, но 6 байтов. В языковом стандарте, где кодировка UTF-8, в

      найти. -имя '????' -exec тире -c '
    имя = $ {1 ## * /}; echo "$ {# name}" 'sh {} \; 
     

find успешно найдет файлы, имя которых состоит из 4 символов в кодировке UTF-8, но тире ] будет указывать длину от 4 до 24.

  1. yash : наоборот. Он работает только с символами . Все вводимые данные внутренне переводятся в символы. Это делает оболочку наиболее согласованной, но это также означает, что она не может справиться с произвольными последовательностями байтов (теми, которые не преобразуются в допустимые символы). Даже в языковом стандарте C он не справляется со значениями байтов выше 0x7f.

      находят.-exec yash -c 'echo "$ 1"' sh {} \; 
     

в языковом стандарте UTF-8 не будет работать с нашим ISO-8859-1 côté.txt из более ранней версии. например.

  1. Такие, как bash или zsh , где постепенно добавлялась поддержка многобайтовой информации. Они вернутся к рассмотрению байтов, которые нельзя сопоставить с символами, как если бы они были символами. У них все еще есть несколько ошибок здесь и там, особенно с менее распространенными многобайтовыми кодировками, такими как GBK или BIG5-HKSCS (они довольно неприятны, поскольку многие из их многобайтовых символов содержат байты в диапазоне 0-127 (например, символы ASCII) ).

  2. Такие, как 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, чтобы файл не был выбран.

2
29.05.2017, 09:25
2 ответа
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выходит из строя (по тайм-ауту ), цикл продолжается.

0
27.01.2020, 21:58

Следует иметь в виду, что оболочки или любые приложения, которые вы обычно запускаете в терминале, не взаимодействуют с клавиатурой и экраном.

Они принимают ввод со своего стандартного ввода в виде потока байтов. Когда этот stdin приходит из обычного файла, байты поступают оттуда, когда это канал, это данные, которые обычно отправляются другим процессом, когда это какой-то файл устройства, который может поступать на физические устройства, подключенные к компьютеру. Например, когда это символьное устройство tty, это данные, отправляемые по некоторой последовательной линии, как правило, терминалом. Терминал — это устройство, которое преобразует события клавиатуры в последовательности байтов.

Вот в чем заключается вся мощь терминальных приложений. Механизм ввода для них абстрагирован, поэтому их можно использовать интерактивно или автоматически в сценариях.

Здесь, если вы собираетесь выдавать такое приглашение и ожидаете событие нажатия клавиши , вы, вероятно, захотите, чтобы ваше приложение (ваш скрипт )был только интерактивным. Либо ожидайте, что стандартный ввод будет терминалом, либо примите ввод с терминала независимо от того, на чем открыт стандартный ввод.

Теперь, как показано выше, все приложения видят потоки байтов, это терминал (или эмулятор терминала )и роль дисциплины линии устройства tty для преобразования нажатой клавиши в последовательности байтов. Несколько примеров:

  • когда вы нажимаете клавишу a , терминалы ASCII отправляют 0x61 байт,
  • когда вы нажимаете клавишу £ , терминалы UTF -8 отправляют два байта 0xc2 и 0xa3.
  • Когда вы нажимаете клавишу Enter , терминалы ASCII отправляют байт 0x0d, который tty line дисциплинирует в системах на основе ASCII -, таких как Linux, обычно преобразуется в 0x0a
  • Когда вы нажимаете только Ctrl , терминалы ничего не отправляют, но если вы нажимаете его с C ,терминалы отправляют байт 0x03, который перехватывается дисциплиной линии tty для отправки сигнала SIGINT задаче переднего плана
  • Когда вы нажимаете Влево , терминалы обычно отправляют последовательность байтов, (варьируется в зависимости от терминала, приложения могут запрашивать базу данных terminfo для ее перевода ), первый из которых 0x1b. Например, в зависимости от режима, в котором он находится, xtermв системах на основе ASCII -отправит либо 0x1b 0x4f 0x44, либо 0x1b 0x5b 0x44(<ESC>[Aили<ESC>OA).

Итак, вот вопросы, которые я бы задал:

  1. Вы все еще хотите запрашивать пользователя, если стандартный ввод не является терминалом
  2. Если ответ на 1 положительный, то вы хотите отправить запрос пользователю на терминале или через stdin/stdout?
  3. Если ответ на 1 отрицательный, вы все еще хотите ждать 5 секунд между каждой итерацией?
  4. Если ответ на 2 — через терминал , должен ли сценарий прерваться, если он не может обнаружить управляющий терминал, или вернуться в нетерминальный режим -?
  5. Вы хотите учитывать только клавиши, нажатые после того, как вы выдали подсказку. IOW, если пользователь случайно введет ключ до того, как будет выдано приглашение.
  6. На что вы готовы пойти, чтобы убедиться, что вы читаете только байты, выданные для одного нажатия клавиши?

Здесь я предполагаю, что вы хотите, чтобы ваш сценарий был только интерактивным приложением терминала и взаимодействовал только через управляющий терминал, оставляя стандартный ввод/стандартный вывод в покое.

#! /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
4
27.01.2020, 21:58

Теги

Похожие вопросы