Почему использование цикла оболочки для обработки текста считается плохой практикой?

Использует while цикл для обработки обычно рассматриваемого текста плохая практика в оболочках POSIX?

Как Stéfane Chazelas указал на , некоторые из причин, по которым не используется оболочка оболочки, концептуальные , надежность , разборчивость , производительность и безопасность .

Этот ответ объясняет надежность и разборчивость :

while IFS= read -r line <&3; do
  printf '%s\n' "$line"
done 3< "$InputFile"

Для производительности цикл while и read чрезвычайно медленны при чтении из файла или канала, потому что читать встроенную оболочку читает один символ за раз.

Как насчет концептуальных концептуальных и безопасности ?

159 голосов | спросил cuonglm 24 12014vEurope/Moscow11bEurope/MoscowMon, 24 Nov 2014 19:28:11 +0300 2014, 19:28:11

4 ответа


209

Да, мы видим несколько таких вещей, как:

while read line; do
  echo $line | cut -c3
done

Или хуже:

for line in `cat file`; do
  foo=`echo $line | awk '{print $2}'`
  echo whatever $foo
done

(не смейтесь, я видел много таких).

Как правило, от начинающих сценариев. Это наивные литературные переводы того, что вы сделали бы на императивных языках, таких как C или python, но это не так, как вы делаете что-то в оболочках, и эти примеры очень неэффективны, полностью ненадежны (потенциально приводят к проблемам безопасности), и если вы когда-либо управляете чтобы исправить большинство ошибок, ваш код становится неразборчивым.

Концептуально

В C или большинстве других языков строительные блоки всего лишь на один уровень выше компьютерных инструкций. Вы говорите своему процессору, что делать, а затем, что делать дальше. Вы берете свой процессор за руку и микро-управляете им: вы открываете этот файл, вы читаете, что много байтов, вы делаете это, вы делаете это с ним.

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

Одна из замечательных вещей, которые представил Unix, - это pipe и те потоки stdin /stdout /stderr по умолчанию, которые все команды обрабатывают по умолчанию.

Через 45 лет мы не нашли лучшего, чем этот API, чтобы использовать силу команд и сотрудничать с ними. Вероятно, это основная причина, по которой люди все еще используют оболочки.

У вас есть режущий инструмент и инструмент транслитерации, и вы можете просто сделать:

cut -c4-5 < in | tr a b > out

Оболочка просто выполняет сантехнику (открывайте файлы, настраиваем трубы, вызываем команды), и когда все готово, оно просто течет без оболочки. Инструменты выполняют свою работу одновременно, эффективно в своем собственном темпе с достаточной буферизацией, чтобы не блокировать другую, это просто красиво и все же так просто.

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

Вызов cut - это как открыть кухонный ящик, взять нож, использовать его, вымыть, высушить, поместить обратно в ящик. Когда вы выполните:

while read line; do
  echo $line | cut -c3
done < file

Это похоже на каждую строку файла, получая инструмент read из кухонного ящика (очень неуклюжий, потому что он не предназначен для этого ), прочитайте строку, вымойте свой инструмент для чтения, верните его в ящик. Затем запланируйте встречу для инструмента echo и cut, получите их из ящика, вызовите их, вымойте, высушите, верните в ящик и так далее.

Некоторые из этих инструментов (read и echo) встроены в большинство оболочек, но это вряд ли имеет значение здесь, поскольку echo и cut все еще нужно запускать в отдельных процессах.

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

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

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

Далее читайте прекрасный ответ Брюса . Внутренние инструменты обработки текста на нижнем уровне в оболочках (кроме, возможно, для cut) ограничены, громоздки и вообще не подходят для общей обработки текста.

Производительность

Как говорилось ранее, запуск одной команды имеет стоимость. Огромная стоимость, если эта команда не встроена, но даже если они встроены, стоимость большая.

И оболочки не предназначены для такого запуска, они не претендуют на то, чтобы быть перформативными языками программирования. Это не так, они просто интерпретаторы командной строки. Таким образом, на этом фронте была сделана небольшая оптимизация.

Кроме того, оболочки запускают команды в отдельных процессах. Эти строительные блоки не имеют общей памяти или состояния. Когда вы выполняете fgets () или zsh в C, это функция в stdio. stdio хранит внутренние буферы для ввода и вывода для всех функций stdio, чтобы избежать слишком дорогостоящих системных вызовов.

Соответствующие даже встроенные утилиты оболочки (fgets(), fputs(), read)) не могут этого сделать. echo предназначен для чтения одной строки. Если он читает прошлую строку новой строкисимвол, это означает, что следующая команда, которую вы запустите, пропустит ее. Таким образом, printf должен читать ввод по одному байту за раз (некоторые реализации имеют оптимизацию, если вход является обычным файлом, поскольку они читают фрагменты и возвращаются, но это работает только для обычных файлов и bash, например, читает только 128 байтовых фрагментов, которые все еще намного меньше, чем текстовые утилиты).

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

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

Между этим циклом read и (предположительно) эквивалентным bash, в моем быстром тестировании в моих тестах есть отношение времени процессора около 40000 (одна секунда против половины дня). Но даже если вы используете только встроенные оболочки:

echo

(здесь с while read), это все равно около 1: 600 (одна секунда против 10 минут).

Надежность /разборчивость

Очень сложно получить этот код. Примеры, которые я дал, слишком часто встречаются в дикой природе, но у них много ошибок.

cut -c3 < file - удобный инструмент, который может делать много разных вещей. Он может читать ввод от пользователя, разделить его на слова для хранения в разных переменных. while read line; do echo ${line:2:1} done выполняет not чтение строки ввода, или, может быть, она читает строку очень специальным образом. На самом деле он читает words из ввода те слова, разделенные символом bash и где обратная косая черта может использоваться для выхода из разделителей или символа новой строки.

Со значением по умолчанию read, на входе типа:

read line

$IFS сохранит $IFS в foo\/bar \ baz biz , а не read line, как и следовало ожидать.

Чтобы прочитать строку, вам действительно нужно:

"foo/bar baz"

Это не очень интуитивно, но так оно и есть, помните, что оболочки не предназначались для использования таким образом.

То же самое для $line. " foo\/bar \" расширяет последовательности. Вы не можете использовать его для произвольного содержимого, такого как содержимое случайного файла. Здесь вам нужно IFS= read -r line .

И, конечно, есть типичный забывающий процитировать вашу переменную , к которой все попадают. Так что это больше:

echo

Теперь еще несколько оговорок:

  • кроме echo, это не работает, если вход содержит NUL-символы, в то время как у текстовых утилит GNU не будет проблем.
  • если есть данные после последней строки новой строки, они будут пропущены
  • внутри цикла, stdin перенаправляется, поэтому вам нужно обратить внимание, что команды в нем не читаются из stdin.
  • для команд внутри циклов, мы не обращаем внимания на то, успешны они или нет. Обычно ошибки с ошибкой (диск полностью, чтение ошибок ...) будут плохо обрабатываться, как правило, хуже, чем с эквивалентом correct .

Если мы хотим рассмотреть некоторые из этих проблем выше, это будет выглядеть следующим образом:

printf

Это становится все менее понятным.

Существует ряд других проблем с передачей данных командам через аргументы или их вывод в переменных:

  • ограничение размера аргументов (некоторые реализации текстовой утилиты также имеют предел, хотя эффект тех, которые были достигнуты, обычно менее проблематичны)
  • символ NUL (также проблема с текстовыми утилитами).
  • аргументы, используемые в качестве параметров, когда они начинаются с while IFS= read -r line; do printf '%s\n' "$line" | cut -c3 done < file (или zsh))
  • различные причуды различных команд, которые обычно используются в таких циклах, как while IFS= read -r line <&3; do { printf '%s\n' "$line" | cut -c3 || exit } 3<&- done 3< file if [ -n "$line" ]; then printf '%s' "$line" | cut -c3 || exit fi , - ...
  • (ограниченные) операторы манипулирования текстами разных оболочек, которые обрабатывают многобайтные символы непоследовательными способами.
  • ...

Вопросы безопасности

Когда вы начинаете работать с командами variables и для команд , вы вводите поле мины.

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

Когда вы можете использовать циклы.

TBD

ответил Stéphane Chazelas 25 22014vEurope/Moscow11bEurope/MoscowTue, 25 Nov 2014 01:50:44 +0300 2014, 01:50:44
37

Что касается концептуальной и удобочитаемости, оболочки обычно интересуются файлами. Их «адресная единица» - это файл, а «адрес» - это имя файла. Оболочки имеют всевозможные методы тестирования для существования файла, типа файла, форматирования имени файла (начиная с подстановки). В оболочках очень мало примитивов для обработки содержимого файла. Программистам оболочки необходимо вызвать другую программу для обработки содержимого файла.

Из-за ориентации файла и имени файла, как вы заметили, обработка текста в оболочке очень медленная, но также требует нечеткого и искаженного стиля программирования.

ответил Bruce Ediger 24 12014vEurope/Moscow11bEurope/MoscowMon, 24 Nov 2014 19:41:03 +0300 2014, 19:41:03
22

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

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

Для первых частей (initialization) обычно не имеет значения, что команды оболочки медленны - это всего лишь несколько десятков команд, возможно, с несколькими короткими циклами. Даже если мы напишем эту часть неэффективно, для выполнения этой инициализации обычно требуется меньше секунды, и это нормально - это происходит только один раз.

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

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

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

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

Я также попытался бы придерживаться стандартных инструментов, доступных на большинстве систем, и стараюсь, чтобы мое использование переносилось, хотя это не всегда возможно. И если ваш любимый язык - Python или Ruby, возможно, вы не будете возражать против того, чтобы убедиться, что он установлен на каждой платформе, на которой должно работать ваше программное обеспечение: -)

Простые инструменты включают head, tail, grep, sort, cut , tr, sed, join (при слиянии двух файлов) и awk однострочные, среди многих других , Удивительно, что некоторые люди могут делать с сопоставлением с образцом и командами sed.

Когда он становится более сложным, и вам действительно нужно применить некоторую логику к каждой строке, awk - хороший вариант - либо однострочный (некоторые люди ставят целые awk-скрипты в одну строку ', хотя это не очень читаемо) или в коротком внешнем скрипте.

Поскольку awk - это интерпретируемый язык (например, ваша оболочка), удивительно, что он может делать линейную обработку так эффективно, но для этого он предназначен для этого, и это очень быстро.

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

И, наконец, есть хороший старый C, если вам нужна максимальная скорость и высокая гибкость (хотя обработка текста немного утомительна). Но, вероятно, очень плохо использовать свое время для написания новой программы на C для каждой задачи обработки файлов, с которой вы сталкиваетесь. Я много работаю с файлами CSV, поэтому я написал несколько общих утилит в C, которые я могу повторно использовать во многих разных проектах. По сути, это расширяет диапазон «простых, быстрых инструментов Unix», которые я могу вызывать из своих сценариев оболочки, поэтому я могу обрабатывать большинство проектов только за счет написания сценариев, что намного быстрее, чем запись и отладка индивидуального кода C каждый раз!

Некоторые окончательные подсказки:

  • Не забудьте запустить свой основной сценарий оболочки с помощью export LANG=C, или многие инструменты будут обрабатывать ваши файлы с обычным ASCII как Unicode, делая их намного медленнее
  • также рассмотрите настройку export LC_ALL=C, если вы хотите sort создать согласованный порядок, независимо от среды!
  • , если вам нужно sort ваши данные, это, вероятно, потребует больше времени (и ресурсов: процессор, память, диск), чем все остальное, поэтому постарайтесь свести к минимуму количество sort и размер файлов, которые они сортируют.
  • , когда это возможно, единственный конвейер, как правило, наиболее эффективен - запуск нескольких конвейеров последовательно, с промежуточными файлами, может быть более читабельным и отладочным, но увеличит время, в течение которого ваша программа займет
ответил Laurence Renshaw 28 52014vEurope/Moscow11bEurope/MoscowFri, 28 Nov 2014 05:53:10 +0300 2014, 05:53:10
9

Да, но ...

правильный ответ Sté phane Chazelas основан на концепция делегирования каждой текстовой операции в определенные двоичные файлы, например grep, awk, sed и другие.

Как способен много сделать вещей сам по себе, падение forks может ускориться (даже если запустить другой интерпретатор для выполнения всей работы).

Для примера, посмотрите на это сообщение:

https://stackoverflow.com/a/38790442/1765658

и

https://stackoverflow.com/a/7180078/1765658

проверить и сравнить ...

Конечно,

Нет никакого мнения о пользовательском вводе и безопасности !

Не записывайте веб-приложение в !!

Но для многих задач администрирования сервера, где можно использовать вместо , использование встроенных bash может быть очень эффективным.

Мой смысл:

Инструменты записи, такие как bin utils , - это не такая же работа, как администрирование системы.

Значит, не одни и те же люди!

Где sysadmins должны знать shell, они могли бы написать прототипы , используя свой предпочтительный (и самый известный) инструмент.

Если эта новая утилита (прототип) действительно полезна, некоторые другие люди могут разработать специализированный инструмент, используя еще более присвоенный язык.

ответил F. Hauri 5 PM00000040000003631 2016, 16:35:36

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

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132