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

196

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

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

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

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

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

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

cuonglm
источник
Связанный (другая сторона медали): Как yesзаписать в файл так быстро?
Уайлдкарт
1
Встроенная оболочка read не читает по одному символу за раз, она читает по одной строке за раз. wiki.bash-hackers.org/commands/builtin/read
A.Danischewski
@ A.Danischewski: Это зависит от вашей оболочки. В bash, он читает один размер буфера за раз, попробуйте, dashнапример. См. Также unix.stackexchange.com/q/209123/38906
cuonglm

Ответы:

256

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Между этим 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

Стефан Шазелас
источник
24
Ясный (ярко), читаемый и чрезвычайно полезный. Спасибо еще раз. На самом деле это лучшее объяснение, которое я когда-либо видел в Интернете, для фундаментального различия между сценариями оболочки и программированием.
Подстановочный
2
Именно такие сообщения помогают новичкам узнать о сценариях оболочки и увидеть их тонкие различия. Следует добавить ссылающуюся переменную как $ {VAR: -default_value}, чтобы гарантировать, что вы не получите нулевое значение. и установите -o nounset, чтобы кричать на вас при обращении к неопределенному значению.
unsignedzero
6
@ A.Danischewski, я думаю, ты упускаешь суть. Да, cutнапример, это эффективно. cut -f1 < a-very-big-fileэффективен, так же эффективен, как если бы вы написали его на C. То, что ужасно неэффективно и подвержено ошибкам, вызывается cutдля каждой строки a-very-big-fileв цикле оболочки, что и было сделано в этом ответе. Это совпадает с вашим последним утверждением о написании ненужного кода, что заставляет меня задуматься, может быть, я не понимаю ваш комментарий.
Стефан Шазелас
5
«За 45 лет мы не нашли лучше, чем этот API, чтобы использовать всю мощь команд и заставить их взаимодействовать для выполнения какой-либо задачи». - на самом деле PowerShell, например, решил страшную проблему синтаксического анализа, передавая структурированные данные, а не потоки байтов. Единственная причина, по которой оболочки еще не используют его (идея существует уже довольно давно и в основном кристаллизовалась когда-то вокруг Java, когда ставшие теперь стандартными типы контейнеров списков и словарей стали мейнстримом) - их сопровождающие пока не могут договориться о общий формат структурированных данных для использования (.
ivan_pozdeev
6
@OlivierDulac Я думаю, это немного юмора. Этот раздел будет навсегда отложен.
Муру
43

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

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

Брюс Эдигер
источник
25

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • не забудьте запустить свой основной сценарий оболочки export LANG=C, иначе многие инструменты будут обрабатывать ваши обычные ASCII-файлы как Unicode, делая их намного медленнее
  • Также подумайте о настройке, export LC_ALL=Cесли вы хотите sortпроизводить последовательный порядок, независимо от среды!
  • если вам нужны sortваши данные, это, вероятно, займет больше времени (и ресурсов: процессор, память, диск), чем все остальное, поэтому постарайтесь свести к минимуму количество sortкоманд и размер файлов, которые они сортируют
  • один конвейер, когда это возможно, обычно наиболее эффективен - последовательный запуск нескольких конвейеров с промежуточными файлами может быть более читабельным и отладочным, но увеличит время, необходимое вашей программе
Лоуренс Реншоу
источник
6
Конвейеры многих простых инструментов (особенно упомянутых, таких как head, tail, grep, sort, cut, tr, sed, ...) часто используются без необходимости, особенно если у вас уже есть экземпляр awk в этом конвейере, который может выполнять задачи этих простых инструментов, а также. Другая проблема, которую следует учитывать, заключается в том, что в конвейерах нельзя просто и надежно передавать информацию о состоянии от процессов на передней стороне конвейера к процессам, которые появляются на задней стороне. Если вы используете для таких конвейеров простых программ awk-программу, у вас есть единое пространство состояний.
Янис
14

Да, но...

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

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

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

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

а также

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

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

Конечно

Там нет соображений о вводе пользователя и безопасности !

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

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

Мой смысл:

Написание таких инструментов, как bin utils , не такая же работа, как системное администрирование.

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

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

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

Ф. Хаури
источник
1
Хороший пример. Ваш подход, безусловно, более эффективен, чем lololux, но обратите внимание, что ответ tenibai (правильный способ сделать это IMO, то есть без использования циклов оболочки) на несколько порядков быстрее, чем ваш. И ваш намного быстрее, если вы не используете bash. (в 3 раза быстрее с ksh93 в моем тесте на моей системе). bashкак правило, самая медленная оболочка. Даже zshв два раза быстрее по этому сценарию. У вас также есть несколько проблем с не заключенными в кавычки переменными и использованием read. Таким образом, вы на самом деле иллюстрируете многие из моих пунктов здесь.
Стефан Шазелас
@ StéphaneChazelas Я согласен, bash - это, пожалуй, самая медленная оболочка, которую люди могут использовать сегодня, но в любом случае наиболее широко используемая.
Ф. Хаури
@ StéphaneChazelas Я опубликовал версию perl в своем ответе
Ф. Хаури,
1
@Tensibai, вы найдете POSIXsh , Awk , Sed , grep, ed, ex, cut, sort, join... все с большей надежностью , чем Bash или Perl.
Подстановочный
1
@Tensibai, из всех систем, относящихся к U & L, большинство из них (Solaris, FreeBSD, HP / UX, AIX, большинство встроенных систем Linux ...) не поставляются с bashустановленными по умолчанию. bashвстречается в основном только на Apple MacOS и систем GNU (я предполагаю , что это то , что вы называете основных дистрибутивов ), хотя многие системы также имеют его в качестве дополнительного пакета (например zsh, tcl, python...)
Stéphane Chazelas