Да, мы видим ряд вещей, таких как:
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
yes
записать в файл так быстро?