Так, ми бачимо низку речей, таких як:
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 a b > out
Оболонка просто виконує сантехніку (відкриває файли, налаштовує труби, викликає команди), і коли все готово, воно просто тече, без оболонки нічого не робити. Інструменти роблять свою роботу одночасно, ефективно у своєму власному темпі з достатньою кількістю буферизації, щоб не блокувати іншого, це просто красиво і при цьому так просто.
Хоча інструмент викликає хоч і коштує (і ми це розвинемо в точці ефективності). Ці інструменти можуть бути написані тисячами інструкцій в C. Потрібно створити процес, інструмент потрібно завантажити, ініціалізувати, потім очистити, процес знищити і чекати.
Викликати cut
- це як відкрити кухонну шухляду, взяти ніж, користуватися ним, вимити, висушити, покласти назад у шухляду. Коли ви робите:
while read line; do
echo $line | cut -c3
done < file
Це як для кожного рядка файлу, дістаючи read
інструмент з кухонного ящика (дуже незграбний, тому що він не призначений для цього ), прочитати рядок, помити інструмент для читання, покласти його назад у шухляду. Потім заплануйте зустріч для інструменту echo
та cut
інструменту, дістаньте їх з шухляди, попросіть їх, вимийте, висушіть, покладіть назад у шухляду тощо.
Деякі з цих інструментів ( read
і echo
) вбудовані в більшість оболонок, але це навряд чи має значення, оскільки їх echo
і cut
досі потрібно запускати в окремих процесах.
Це як нарізати цибулю, але випрати ніж і покласти назад в кухонну шухляду між кожним шматочком.
Тут очевидний спосіб - дістати свій cut
інструмент з ящика, нарізати всю цибулину і покласти її назад в ящик після того, як буде виконана вся робота.
IOW, в оболонках, особливо для обробки тексту, ви викликаєте якомога менше утиліт і змушуєте їх співпрацювати із завданням, а не запускати тисячі інструментів послідовно, очікуючи, що кожен з них запуститься, запуститься та очиститься перед запуском наступного.
Подальше прочитання в тонкій відповіді Брюса . Внутрішні інструменти для обробки тексту в оболонках низького рівня (крім, можливо, для них 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
...
- оператори з маніпулювання текстом різних оболонок, які обробляють багатобайтові символи непослідовно.
- ...
Міркування щодо безпеки
Коли ви починаєте працювати зі змінними оболонки та аргументами команд , ви вводите мінне поле.
Якщо ви забудете цитувати свої змінні , забудете кінець маркера опцій , працюйте в локалях з багатобайтовими символами (норма цих днів), ви впевнені, що введете помилки, які рано чи пізно стануть вразливими.
Коли ви хочете використовувати петлі.
ТБД
yes
записати файл у файл так швидко?