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

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

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

Этот ответ объясняет аспекты надежности и четкости :

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

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

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

Да, мы видим ряд вещей, таких как:

 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, был канал и те потоки stdin / stdout / stderr по умолчанию, которые все команды обрабатывают по умолчанию.

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

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

 cut -c4-5 < in | tr ab > out 

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

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

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

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

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

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

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

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

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

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

Представление

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

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

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

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

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

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

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

 while read line; do echo ${line:2:1} done 

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

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

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

read – удобный инструмент, который может делать много разных вещей. Он может читать ввод от пользователя, разделить его на слова для хранения в разных переменных. read line не читает строку ввода, или, может быть, она читает строку очень специальным образом. Он на самом деле читает слова из ввода этих слов, разделенных $IFS и где обратная косая черта может использоваться для удаления разделителей или символа новой строки.

При значении по умолчанию $IFS на входе, например:

  foo\/bar \ baz biz 

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

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

 IFS= read -r line 

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

То же самое для echo . echo расширяет последовательности. Вы не можете использовать его для произвольного содержимого, такого как содержимое случайного файла. Здесь вам нужен printf .

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

 while IFS= read -r line; do printf '%s\n' "$line" | cut -c3 done < file 

Теперь еще несколько предостережений:

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

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

 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 

Это становится все более и более разборчивым.

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

  • ограничение на размер аргументов (некоторые реализации текстовой утилиты также имеют предел), хотя влияние достигнутых, как правило, менее проблематично)
  • символ NUL (также проблема с текстовыми утилитами).
  • аргументы, принятые в качестве опций, когда они начинаются с - (или иногда)
  • различные причуды различных команд, которые обычно используются в таких циклах, как expr , test
  • (ограниченные) операторы манипулирования текстами различных оболочек, которые обрабатывают многобайтные символы непоследовательным образом.

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

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

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

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

TBD

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Некоторые окончательные намеки:

  • не забудьте запустить свой основной сценарий оболочки с помощью export LANG=C , или многие инструменты будут обрабатывать ваши файлы с обычным файлом ASCII как Unicode, делая их намного медленнее
  • также рассмотрите настройку export LC_ALL=C если вы хотите, чтобы sort производила согласованный порядок, независимо от среды!
  • если вам нужно sort данные, это, вероятно, потребует больше времени (и ресурсов: процессор, память, диск), чем все остальное, поэтому старайтесь минимизировать количество команд sort и размер файлов, которые они сортируют
  • по возможности, единственный трубопровод, как правило, наиболее эффективен – запуск нескольких конвейеров последовательно, с промежуточными файлами, может быть более читабельным и отладочным, но увеличит время, которое займет ваша программа

Да, но…

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

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

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

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

а также

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

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

Конечно

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

Не пишите веб-приложение под bash !!

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

Мой смысл:

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

Так не одни и те же люди!

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

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