підказка: скрипти bash без sed/cut/awk

| комп'ютери, linux, підказки, bash

в розділі tips, tricks and scripts англомовного форуму crunchbang знайшов цю підказку за авторством imbecil: як уникнути використання (чудових!) утиліт sed, cut і awk для нескладної обробки текстових рядків у bash-скриптах. стаття досить детально викладена, з прикладами — тож вирішив перекласти повністю і зберегти тут собі на згадку.

може ще комусь стане в пригоді? тоді не полінуйтеся, будь ласка, зареєструватися на форумі й подякувати авторові =)

1. вступ
2. приклади
  2.1. спосіб «sed+cut»
  2.2. спосіб «лише bash»
3. швидкодія
  3.1. тестовий скрипт
  3.2. результати тестування швидкодії
4. заключне слово

1. вступ

що?

чимало скриптів bash часто потребують інформації з текстових змінних (або з виводу команд), приміром:

variable="   якийсь текст XXXX-YYYY-ZZ 0xab2345 траляля"
#         ^^^ зауважте пробіли на початку текстового рядка


потребуючи й сам такого і шукаючи рецептів у тенетах, я помітив, що більшість пропонованих рішень використовують утиліти sed, cut та/або awk в різних комбінаціях. найчастіше текстовий рядок спрощують за допомогою sed, і згодом «вирізають» потрібну частину рядка за допомогою cut чи awk.

чому?

будучи впертим (уникаю використання python/ruby/perl) пуристом (навіщо викликати сторонні утиліти на кшалт sed у випадках, коли bash і сам достатньо потужний), я натрапив на згадку про «дивовижну кількість команд для операцій з текстовими рядками» та «масиви», інтегровані у bash, і відкрив для себе, що чимало операцій можна запрограмувати за допомогою цих команд замість вдаватися до запуску sed, cut і awk. а оскільки більшість пошукових запитів у google приводять саме на рецепти з sed/cut/awk, мені видалося непоганою ідеєю порекламувати інтегровані команди bash за допомогою кількох прикладів.

як?

робиться це навдивовижу просто, і, на мою гадку, семантично елегантніше в порівнянні до рецептів з sed, cut і awk. є певні недоліки, зокрема деякі однорядні команди (one-liners) неможливо зконструювати в силу особливостей роботи команд bash. для обробки тексту і масивів, проте загальне враження таке, що використання інтегрованих команд bash робить скрипти більш зрозумілими для читання.

далі йтимуть декілька прикладів…

2. приклади

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

1680x1050+2880+23


звісно, неважко впізнати звичний рядок з «геометрією» дисплея, такі видає команда xrandr.

2.1 спосіб «sed+cut»

ось простий скрипт, щоби зробити це за допомогою sed:

# приклад SED
# задаймо текстовий рядок
xrandroutput="1680x1050+2880+23"

# визначмо символ табуляції TAB ('\t')
# знадобиться для sed та cut
TAB=$(echo -e "\t")

# замінімо «x» на тауляцію '\t'
array=`echo "$xrandroutput" | sed "s/x/$TAB/"`

# замінімо «+» на табуляцію '\t'
array=`echo -e "$array" | sed "s/+/$TAB/g"`

# збережімо потрібні значення
H=`echo -e "$array" | cut -f 1`
W=`echo -e "$array" | cut -f 2`
X=`echo -e "$array" | cut -f 3`
Y=`echo -e "$array" | cut -f 4`


гакери скрикнуть: «навіщо sed двічі?!» — і матимуть рацію, скрипт можна трошки скоротити:


# приклад SED
# задаймо текстовий рядок
xrandroutput="1680x1050+2880+23"

# визначмо символ табуляції TAB ('\t')
# знадобиться для sed та cut
TAB=$(echo -e "\t")

# замінімо «x» і «+» на тауляцію '\t'
array=`echo "$xrandroutput" | sed "s/[x+]/$TAB/g"`

# збережімо потрібні значення
H=`echo -e "$array" | cut -f 1`
W=`echo -e "$array" | cut -f 2`
X=`echo -e "$array" | cut -f 3`
Y=`echo -e "$array" | cut -f 4`


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

2.2 спосіб «лише bash»

а тепер обіцяний приклад використання лише інтегрованих команд bash:

# приклад ARRAY
# задаймо текстовий рядок
xrandroutput="1680x1050+2880+23"

# замінімо всі «x» на пробіли « », скориставшись
# потужною інтегрованою в bash командою заміни
# тексту за шаблоном:
# ${string//substring/replacement},
# і збережімо результат до змінної «array»
# (поки що це не масив)
array=${xrandroutput//x/" "} # результат: "1680 1050+2880+23"

# замінімо всі «+» на пробіли « » і збережімо
# результат тепер як масив у змінній «array»,
# використавши дужки «(» і «)»
# заувага: дужки приймають пробіл як роздільник
# і таким чином створюють масив значень
array=( ${array//+/" "} ) # результат: ( "1680" "1050" "2880" "23" )

# надрукуймо результат
echo "array[0] = ${array[0]}"
echo "array[1] = ${array[1]}"
echo "array[2] = ${array[2]}"
echo "array[3] = ${array[3]}"


звісно ж дві команди заміни можна об’єднати в одну:

# приклад ARRAY-SINGLE
...
# замінімо всі «x> і «+» на пробіли « »
# і збережімо результат в масив з назвою «array»
array=( ${xrandroutput//[x+]/" "} )
...


ну хіба ж не простіше і зрозуміліше? вже мовчу про те, що на виході всі дані маємо в одному масиві, а це досить зручно і не захаращує код назвами змінних.

аж зголоднів, поки писав це. мушу щось з’їсти і хильнути пива, перш ніж перейти до тестування швидкодії.

3. швидкодія

3.1 тестовий скрипт

чи справді варто використовувати інтегровані команди bash для обробки рядків і масивів, — а чи може це лише альтернативний спосіб і не більше? можна прогнати кілька тестів і подивитися, скільки за часом виконується кожен варіант. спершу я поясню, як саме я тестував. далі подаю чернетку тестового скрипта. ідея проста: запустити декілька (багато) подібних прикладів і заміряти час виконання.

#!/bin/bash
# використання: test [кількість_ітерацій]

iter=10000
# перевірмо, чи задано кількість ітерацій в командному рядку
if [ -n "$1" ]
then
iter="$1"
fi

TAB=$(echo -e "\t")

# проженімо ітерації
for i in `seq $iter`
do
#
# робочий код для тестування
#
done


цей тестовий скрипт запускається таким чином:

$ /usr/bin/time -f "\nReal: %E\nUser: %U\nSys: %S" ./test 100000


всередині циклу «do … done» вміщуємо тестовий код (я прибрав коментарі задля компактності):

# приклад SED
xrandroutput="1680x1050+2880+23"
array=`echo "$xrandroutput" | sed "s/x/$TAB/"`
array=`echo -e "$array" | sed "s/+/$TAB/g"`
H=`echo -e "$array" | cut -f 1`


а потім другий варіант:

# приклад ARRAY
xrandroutput="1680x1050+2880+23"
array=${xrandroutput//x/" "} # "1680 1050+2880+23"
array=( ${array//+/" "} ) # ( "1680" "1050" "2880" "23" )


зауважте, що у варіанті «приклад SED» ми змушені зберігати результат до змінної «H», тоді як у варіанті «приклад ARRAY» всі чотири значення зберігаються в одному масиві з доступом через «array[i]».

3.2 результати тестування швидкодії

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

а результати такі:

приклад SED: 1000 ітерацій = 2,17 секунд
приклад ARRAY: 1000 ітерацій = 0,011 секунд


так, різниця 200-кратна на користь варіанту «приклад ARRAY». отже, інтегровані команди bash працюють значно швидше, ніж виклик sed.

дивно? ну, я очікував дещо вищої продуктивності, але ж не 200-кратно… насправді, тут я запрошую знавців написати оптимальніший код з використанням sed. можливо, це можна зробити краще, ніж вийшло у мене.

4. заключне слово

отже, чи варто користуватися інтегрованими командами bash? спробую підсумувати:

переваги (аргументи на користь внутрішніх команд bash:
+ це значно швидше;
+ код зрозуміліший (на мою думку), з меншою кількістю змінних;
+ мені здається, що це простіше програмувати.

недоліки (проти):
– значна залежність від bash;
– хоча масиви і команди обробки рядків мають бути доступні в сучасних версіях bash (вище третьої), все-таки можуть виникати проблеми сумісності в старших версіях;
– неможливо реалізувати деякі однорядкові команди (one-liners), які досить просто отримати перенаправленням виводу.

наостанок кілька слів:

а) сподіваюся, комусь це стане в нагоді. розумію, що хтось скаже: «о, я це й так знав», інші запитають «про що це він взагалі говорить?», проте я таки сподіваюся, що хтось навчиться чогось нового;
б) перепрошую, що не спромігся пояснити коротше; також перепрошую за кострубату англійську;
в) не соромтеся збиткуватися з мене, якщо я десь тут припустився помилки.

кінець.