Чому використання циклу оболонки для обробки тексту вважається поганою практикою?


196

Чи використовується цикл час для обробки тексту, як правило, поганою практикою в оболонках POSIX?

Як вказував Стефан Шазелас , одні з причин невикористання шлейфу оболонки - концептуальність , надійність , розбірливість , ефективність та безпека .

Ця відповідь пояснює аспекти надійності та розбірливості :

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

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

Як щодо концептуальних та безпекових аспектів?


Пов'язане (інша сторона монети): Як yesзаписати файл у файл так швидко?
Wildcard

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

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

На мові 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...
  • оператори з маніпулювання текстом різних оболонок, які обробляють багатобайтові символи непослідовно.
  • ...

Міркування щодо безпеки

Коли ви починаєте працювати зі змінними оболонки та аргументами команд , ви вводите мінне поле.

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

Коли ви хочете використовувати петлі.

ТБД


24
Чіткий (яскраво), читабельний і надзвичайно корисний. Ще раз дякую Це насправді найкраще пояснення, яке я бачив десь в Інтернеті, щодо принципової різниці між сценаріями оболонок і програмуванням.
Wildcard

2
Такі публікації допомагають новачкам дізнатися про сценарії Shell і побачити, що це тонкі відмінності. Слід додати змінну посилань як $ {VAR: -default_value}, щоб уникнути нуля. і встановіть -о іменний набір кричати на вас при посиланні на не визначене значення.
unsignedzero

6
@ A.Danischewski, я думаю, ви пропускаєте суть. Так, cutнаприклад, є ефективним. cut -f1 < a-very-big-fileЕфективна, настільки ж ефективна, як ви б написали це в C. Що страшенно неефективне і схильний до помилок, викликає cutкожен рядок a-very-big-fileциклу оболонки, який полягає в тому, що робиться в цій відповіді. Це співпадає з вашим останнім твердженням про написання непотрібного коду, що змушує мене думати, можливо, я не розумію ваш коментар.
Стефан Шазелас

5
"За 45 років ми не знайшли кращого за цей API, щоб використати силу команд і змусити їх співпрацювати із завданням". - насправді PowerShell вирішила жахливу проблему розбору, передаючи структуровані дані, а не потоки байтів. Єдина причина, що оболонки ще не використовують її (ідея існувала досить довго і в основному викристалізувалася десь навколо Яви, коли тепер уже стандартні типи контейнерів списку та словника стали основними) - це те, що їхні обслуговуючі ще не могли погодитися загальний структурований формат даних для використання (.
ivan_pozdeev

6
@OlivierDulac Я думаю, що це трохи гумору. Цей розділ буде назавжди TBD.
муру

43

Що стосується концептуальної та розбірливості, оболонки зазвичай зацікавлені у файлах. Їх "адресною одиницею" є файл, а "адреса" - ім'я файлу. Оболонки мають всі види методів тестування на наявність файлів, тип файлу, форматування імен файлів (починаючи з глобулювання). Оболонки мають дуже мало примітивів для роботи з вмістом файлів. Програмісти Shell повинні викликати іншу програму, щоб мати справу з вмістом файлів.

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


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", які я можу викликати зі своїх скриптів оболонки, тому я можу обробляти більшість проектів лише написанням скриптів, що набагато швидше, ніж написання та налагодження кожного разу вимагати використання коду С!

Кінцеві підказки:

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

6
Трубопроводи багатьох простих інструментів (зокрема, згаданих, таких як голова, хвіст, греп, сортування, вирізання, тр, sed, ...) часто використовуються без необхідності, особливо якщо у вас також є екземпляр у цьому конвеєрі, який може зробити завдання цих простих інструментів. Інша проблема, яку слід враховувати, полягає в тому, що в трубопроводах ви не можете просто і надійно передавати інформацію про стан від процесів на передній стороні трубопроводу до процесів, що з'являються на задній стороні. Якщо ви використовуєте для таких конвеєрів простих програм програму awk, у вас є простір єдиного стану.
Яніс

14

Так, але...

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

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

Для зразка подивіться цю публікацію:

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

і

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

перевірити і порівняти ...

Звичайно

Про введення та безпеку користувачів не враховується !

Не пишіть веб-додаток під !!

Але для багатьох завдань адміністрування сервера, де може бути використаний замість , використання вбудованого bash може бути дуже ефективним.

Моє значення:

Інструменти для запису, такі як утиліти, - це не такий самий вид роботи, як адміністрування системи.

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

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

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


1
Хороший приклад. Ваш підхід, безумовно, більш ефективний, ніж lololux, але зауважте, як відповідь тенсібая (правильний спосіб зробити цей ІМО, тобто без використання шлейфових оболонок) набирає порядку на швидкість, ніж ваш. І ваше набагато швидше, якщо ви не користуєтесь bash. (більш ніж у 3 рази швидше з ksh93 в моєму тесті на моїй системі). bashяк правило, найповільніша оболонка. Навіть zshудвічі швидший за цим сценарієм. У вас також є кілька проблем із змінними, які не котируються, та використанням read. Отже, ви насправді ілюструєте тут багато моїх точок.
Стефан Шазелас

@ StéphaneChazelas Я згоден, баш - це, мабуть, найповільніша оболонка, яку люди могли б сьогодні використати, але все-таки найбільш широко використовується.
Ф. Хаурі

@ StéphaneChazelas Я опублікував версію perl на свою відповідь
F. Hauri

1
@Tensibai, ви знайдете POSIXsh , Awk , Sed , grep, ed, ex, cut, sort, join... все з більшою надійністю , ніж Bash або Perl.
Wildcard

1
@Tensibai з усіх систем, які стосуються U&L, більшість із них (Solaris, FreeBSD, HP / UX, AIX, більшість вбудованих систем Linux ...) не bashвстановлені за замовчуванням. bashзустрічається в основному тільки на Apple MacOS і систем GNU (я припускаю , що це те , що ви називаєте основних дистрибутивів ), хоча багато систем також мають його в якості додаткового пакета (наприклад zsh, tcl, python...)
Stéphane Chazelas
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.