Секрети в Bash для початківців: оптимізація для Linux і DevOps

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

На зображенні випадковий код для привернення уваги :)

Привіт всім. Я багато років працюю на різних посадах інфраструктурного інженера та хочу розказати про декілька моментів щодо shell-скриптів.
Мої перші мови — BASIC та Assembler, перші батники ще в DOS та Windows 9x, і на них я робив багато автоматизації адміністрування.

Я взагалі досі вважаю себе прихильником Windows.
Але вважаю, що shell-скриптинг дуже незаслужено недооцінюють. shell-скрипти дуже глибоко проникли в *nix і технічно й ідеологічно, вони виросли разом з *nix, а пізніше з Linux та стандартами POSIX.

Найпопулярніший shell інтерпретатор — це bash. Але озираючись на сумісність, потрібно відрізняти POSIX shell від bash, бо деякі нововведення в bash не підтримуються в більш простих оболонках, таких як sh чи dash. Інши сучасні інтерпретатори zsh, fish — можуть навіть більше, особливо в адмініструванні.
В DevOps практиках я намагаюсь користуватися найбільш поширеними або вже встановленими інструментами для сумісності та скороченню технологічного стеку, тому пишу на bash чи на POSIX-сумісному шелл.
Це важливо, бо в сучасному світі ви можете зустріти саме простіший POSIX shell в embedded чи контейнерах.

У статті я для спрощення буду писати bash, якщо це буде важливо, окремо буду писати POSIX shell.

Зазвичай bash, на відміну від універсальних скриптових мов програмування, використовується найчастіше для роботи з іншими продуктами, а не у ролі самостійних застосунків.

Тобто на bash пишуть різні враппери, адміністративні скрипти для конфігурації та інсталяції, скрипти для бекапу, збірки та створення пакетів, пайплайнів, моніторингові грабери, «клей» для інтеграції різних продуктів, підготовки оточення та інші проміжні речі.
Бо він під рукою майже всюди і є велика кількість потужного консольного софта, який дуже і дуже легко інтегрувати саме в bash-скрипт.

Також в продакшені bash може бути дуже мало, а ось у CI/CD та адмініструванні він зустрічається набагато частіше, тому його знання для адмінів та DevOps-ів обов’язкове.

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

На жаль, багато айтішніків, які вважають bash дуже простим, навіть не намагаються прочитати основи документації. Пишуть навмацьки, тому і роблять багато архітектурних помилок та лаються на неінтуїтивну поведінку.

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

Зміст:

1. Розбиваємо строку
2. Розберемося з перенаправленнями інпуту, а саме <, << та <<<
3. Простий приклад FizzBuzz та навіщо писати #!/usr/bin/env bash
4. Autocomplete
5. read замість sleep
6. Лапки....

1. Розбиваємо строку

Цей приклад зустрічається повсякденно. Треба розбити строку на декілька значень чи викусити щось із структурованої або поганоструктурованої строки. Наприклад, маємо строку «c1 c2 c3». Треба прочитати значення другої колонки.

A) Багато хто використовує cut, просту команду, що створена саме для таких цілей:

$ echo "c1 c2 c3" | cut -d " " -f 2
c2

B) Дехто користується awk, яка підтримує регулярки, має внутрішню мову і може робити багато речей з текстом:

$ echo "c1 c2 c3" | awk '{print $2}'
c2

C) Можна навіть викусити щось регуляркою через grep — тут я беру щось між двух spacers та користуюсь можливостями grep — опція -o:

$  echo "c1 c2 c3" | grep -oP '\s\K\S*(?=\s)'
c2

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

Під Windows ця проблема ще більша, бо архітектура така, що створення нового процесу займає набагато більше ресурсів. Наприклад, в емуляції cygwin це може зайняти на два порядки довше, ніж в Linux чи навіть під Windows WSL.

Це, можливо, не має особливого значення, якщо ви робите якусь одноразову процедуру. Але якщо треба виконати команду у циклі чи це моніторинговий скрипт, що виконується кожну хвилину, то краще його оптимізувати (або навіть обрати perl/python/etc.).

D) Спробуємо розбити строку внутрішньою командою read ось так:

$ read one two three <<< "c1 c2 c3"; echo $two
c2

Для read також можна змінити сепаратор на інший символ через спеціальну змінну IFS (internal field separator) і працювати з CSV:

$ IFS=";" ; read one two three <<< "с1;с2;с3;с4" ; echo $two
c2
В останньому прикладі ми читаєм у три змінні, але маємо чотири колонки. Тому в змінну three буде записана решта значень:
$ IFS=";" ; read one two three <<< "с1;с2;с3;с4" ; echo $three
c3 c4
$ IFS=";" ; read one two three <<< "с1;с2;с3;с4;c5;c6" ; echo $one; echo $two; echo $three
c1
c2
c3 c4 c5 c6

E) Якщо у вас простий випадок, то строку можна легко конвертнути у масив:

$ myarray=($(echo "c1 c2 c3" )); echo "${myarray[1]}"
c2
Чи якщо строка у змінній, конструкція виглядає простіше:
$ string="c1 c2 c3"; myarray=($string); echo "${myarray[1]}"
c2

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

F) Якщо у вас складний випадок неструктурованого тексту, можна використати bash variable expansion, та вирізати необхідне за декілька підходів, і навіть 5-10 variable expansion будуть працювати скоріше за один read (тут ми вирізаємо все до пробілу, потім все після пробілу:

$ A="с1 с2 с3"; b=${A#* }; echo ${b% *}
c2

Ще раз зроблю акцент на тому, що три останні варіанти працюють з внутрішніми командами Shell, тому виконуються дуже швидко. У 100-1000 разів швидше за виклик зовнішньої утиліти cut чи awk.
Нижче приблизні заміри на звичайній віртуалці (2 cpu):

time { for ((i=0;i<1000;i++)); do echo "c1 c2 c3" | cut -d " " -f 2;done }
real    0m3.537s
user    0m1.902s
sys     0m1.277s

time { for ((i=0;i<1000;i++));do read one two three<<<"с1 с2 с3"; echo $two;done }
real    0m0.163s
user    0m0.022s
sys     0m0.032s

time { for ((i=0;i<1000;i++));do  A="с1 с2 с3"; b="${A#* }"; echo "${b% *}";done }
real    0m0.061s
user    0m0.015s
sys     0m0.009s

Також зверніть увагу, що одна команда read виконувалась майже втричі довше, ніж дві команди з variable expansion.

Якщо спробувати це запустити в git-bash чи cygwin/mings під Windows, де робота з терміналом буде емульована, то все це буде на два-три порядки повільніше, та займе вже не секунди а хвилини! (За винятком WSL, бо там віртуальний Linux ;)

2. Розберемося з перенаправленнями інпуту, а саме <, << та <<<

A) Перенаправлення «<» — це перенаправлення з файлу.
Тобто Bash відкриє файл та передасть значення в команду як неіменований пайп. Це одне з найпопулярніших перенаправлень. А ще це корисно, якщо ви хочете приховати ім’я файлу. Наприклад, спробуйте виконати отак:

$ wc -l readme.txt
28 readme.txt
$ wc -l <readme.txt
28

Але в *nix також можна перенапрявляти інформацію з пристріїв, процесів, тощо. Наприклад, генеруємо рандомну строку байт:

read -N10 </dev/random
не забувайте — все є файл ;)

B) Конструкція з «<<» зветься Document Here.
Використовується, коли треба передати якийсь текст в програму, але ви не хочете створювати окремий файл для цього. Тоді ми робимо щось типу «attach» файла прямо в скрипті.

Синтаксис:

cat << EOF
ваш текст
можливо декілька строк
EOF

В цьому випадку EOF — це просто абревіатура (End Of File) для маркера початку та закінчення тексту. Ви можете використати EOF чи будь-яку іншу строку як маркер:

cat << ATTACH
ваш текст
можливо, декілька строк
ATTACH

Для тексту також виконається bash variable expansion (тобто змінні будуть замінені на значення, будуть виконані command substitutions тощо), але вам не потрібно мучитися з ескейпом лапок. Дані будуть передані, як є, що іноді дуже спрощує справу.

Приклад виконання для оракл-клієнта в скрипті:

sqlplus -s <<MYQUERY
  connect user/pass
  select 1 from dual;
  select 1 from dual;
  select "$myvariable" from dual;
  quit;
MYQUERY

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

$ sqlplus -s username/password@host:port/service @myfile.sql

C) Останнє перенаправлення «<<<».
Ця конструкція — просто передача однієї строки в команду. Приклад з тим же sqlplus:

$ sqlplus <<< "select * from dual"

Можна відправити і багаторядковий текст:

$ sqlplus <<< "select * from dual;
select * from dual;
quit;"

Але якщо вам потрібні лапки у вашому квері, то на відміну від Document Here, в даному випадку треба все екранувати.

$ sqlplus <<< "select * from dual;
select \"$myvariabe\" from dual;
quit;"

D) В каментах запропонували розглянути 2>&1

Давайте розглянемо що це, та як користуватись.
Як ми знаємо, за стандартом для процесів відкривається три хендлера 0 — stdin, 1 — stdout, 2 — stderr. Якщо будете відкривати інши, можно брати 3, 4, 5 і так далі.
Перенаправлення підтримуюсть синтаксис

1>, 1>>, 2>, 3>...
А якщо номер не вказаний, то за замовченням буде 1 (stdout).

&1 — це референс до першого хендлеру. 2>&1 — це перенаправити другий хендлер у перший. Тобто stderr в stdout. Але stderr та stdout це не destination з точку зору вже запущеного процесу, це вже відкріти хендлери кудись.
Найчастіше вони ведуть до конкретного терміналу вашої сессії, але може бути і інше. Можете напряму глянути який термінал використано для поточного процесу, наприклад отак:

ls -l /proc/$$/fd

Розберемо три приклади:
find . -name "*.txt" 2>&1
Ця команда каже, щоб перенаправити stderr в stdout, при цьому stdout нікуди не перенаправлен, і на цей момент прикріплений до нашої консолі, тому візуально нічого не зміниться, ми побачимо все виведення команди в консолі.

find . -name "*.txt" >file.log 2>&1
В даному випадку, ми перенаправили stdout в файл file.log, а потім stderr в stdout, таким чином ми просто обидва output виводи перенаправляємо в файл file.log, і це корисно що не треба двічи писати ім’я того ж самого файла.

find . -name "*.txt" 2>&1 >file.log 
Команда схожа на попередню, але на момент перенаправлення stderr в stdout, він ще не був перенаправлений у файл, а просто пов’язан за виводом консолі, тому у нас stdout піде у файл file.log, а stderr піде у консоль. До цього це буде саме як stdout, що дозволяє, наприклад, зробити отакий трюк:
find . -name "*.txt" 2>&1 >file.log | grep "errors"
Це дуже корисно, бо для символа пайп, нема можливості вказати хендлер — синтаксис на кшалт 2| чи 1| не підтримується, тому якщо треба перекинути stderr через пайп, потрібно як раз цей трюк — спочатку перенаправити його в stdout, що я іноді використовую, коли треба обробити саме вихлоп stderr.

P.S. Майже всі стандартні консольні тулзи (особливо gnu tools), коректно виводять information та error/warning повідомлення у відповідні stdout/stderr, тобто на це можно впевнено спиратися. Якщо ви девелопер та пишете консольну тулзу, то це ваша відповідальність писати std::cout чи std::cerr у конкретному випадку.

3. Простий приклад FizzBuzz та навіщо писати #!/usr/bin/env bash

#!/usr/bin/env bash
for i in {1..100}; do
   [ $((i%3)) -eq 0 ] && a[$i]="Fizz"
   [ $((i%5)) -eq 0 ] && a[$i]+="Buzz"
   printf "${a[$i]:-$i} "
done

Ми викликаємо env, яка виконує іншу програму.
Для чого це робиться?
Команда env дуже корисна, коли треба задати змінну, ім’я якої несумісне із Bash variable name convention (наприклад, env ssl.cert.name=/home/test/mycert.crt java -jar myapp.jar).

Але найголовніше, навіщо ми виконуємо bash через env — це запуск bash, який може бути десь в PATH.
В різних дистрибутивах Linux/Unix/MacOS та інше, bash може бути в /bin чи /usr/bin, чи може ще десь, тому запуск через env (який завжди /usr/bin/env) робить наш скрипт більш сумісним із різними системами.

Далі в нашому FizzBuzz, залежно від ситуації ми наповнюємо значення масиву а.
І нарешті виводимо значення масиву a, але якщо там пусто, то виводимо просто індекс i.
В Bash ми можемо виконати операцію = чи += із змінними, але у нас нема -=, /=, *=, тому що в Bash нема строгої типізації змінних. І тому всі операції повинні бути сумісними як з числовими, так і текстовими. += просто виконує конкатенацію текстових змінних, та складає числові.

4. Autocomplete

Мабуть, всі користуються автокомплітом для файлів та каталогів. Але автокомпліт працює не тільки з ними. Ще він автодоповнює імена змінних і функцій, а також аліасів. Тому, якщо ви хочете зробити собі швидкий аліас, можна підібрати ім’я таким чином, щоб його можна було швидко автодоповнити з однієї-двох літер. Наприклад, писати з uppercase-літер.

alias DEBUGLOG="tail -f $HOME/myapp/debug.log"

function DECRYPT() {
  echo '$1' | openssl enc -pass file:$HOME/secret.key -d -aes-256-cbc -a
}

Ну і не забувайте, що функції, покладені в .profile чи в .bashrc також можна визивати інтерактивно. І це непогана заміна для alias, якщо треба виконати щось більше, ніж одну-дві команди, просто пишемо

DEB<tab> чи DEC<tab>
.

5. read замість sleep

sleep дозволяє зробити невеличку паузу. Але якщо замінити його на read -t <time>, то у разі інтерактивного виконання скрипта можна скіпнути цю паузу, натиснувши enter.
А ще цікаво, що read, як і sleep, підтримують долі секунд, тобто можна read -t 0.5 чи sleep 0.2.

Ну і іноді буває потрібно запускати скрипт з дефолтними значеннями чи з кастомними — read з таймаутом дозволить почекати кастомних значень. А якщо ніхто нічого не ввів (або одразу натиснув enter з пустим значенням), то це можна перевірити та замінити пусте значення на дефолтне. І скрипт можна запускати вручну чи автоматично — він просто почекає та автоматично продовжить роботу.

6. Лапки....

На останнє, проблеми з лапками та розкриттями змінних переслідують майже всіх новачків.
Найчастіше можно побачити помилки, що маємо із пустими змінними, чи змінними, чи значення мають пробіли чи інши спеціальні символи.
Приклади:
[ $myvar == "yes" ]
Якщо $myvar пуста, то буде не false а помилка синтаксису.
Якщо у $myvar будуть пробіли — це також буде помилка синтаксису.
Правильно: [ "$myvar" == "yes" ].
Ще можно використовувати продвинутий test у bash: [[ $myvar == "yes" ]], але це несумісно з POSIX.

Розберемо роботу з файлами. Маємо файл с пробілом:

$ find .
.
./test.sh
./hello world.txt

Пробуємо передати список файлів отак:

find . | xargs echo
. ./a.sh ./hello world.txt

Виглядає непогано. Але давайте перевіримо кожний елемент:

find . | xargs -n1 echo
.
./test.sh
./hello
world.txt

Виявляється, що насправді файл з пробілом було порізано на два елементи.
Виправляємо:

find . -print0 | xargs -0 -n1 echo
.
./test.sh
./hello world.txt
Так працює, тому що find та xargs мають опції, щоб змінити сепаратор на нульовий символ.

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

7. Wildcard (маски файлів)

Ще маленька дрібничка про wildcard в *NIX та Windows.
У давні-давні часи, в файловій системі FAT, ім’я файла та розширення — були окремі поля. Тому в ДОС та першіх версіях Windows, всі файли можно було знайти маскою *.*

В *NIX взагалі нема такого як росширення, є просто ім’я файла, в ньому може бути крапка чи крапки. Тому знайти всі файли — це просто *. А якщо спробувати шукати *.*, то в *NIX це файли, в яких є хоча б одна крапка.

Наразі Windows також може знайти все за маскою *, але для забезпечення обратної сумісності, *.* також буде шукати всі файли в Windows, але тільки файли в імені яких є хоча б одна крапка в *NIX.

На сьогодні все, запрошую до коментарів тих, кому цікаві деталі.

P.S. Я повністю розумію, що Bash — це просто інструмент, і, якщо треба, роблю речі на python/perl/helm. Але я вважаю, що спеціаліст повинен розбиратись в bash хоча б на середньому рівні, бо це дійсно крутий і дуже поширений інструмент.

👍ПодобаєтьсяСподобалось27
До обраногоВ обраному22
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Круто. Дякую! Так мало подібних статей тут.

Дякую.
Технічни статті не дуже популярні, бо їх читають лише ті, в кого саме ця технологія використовується в проекті. А ось обсудити HR-ів, підтянуться усі...

Наскільки я бачу проблему ДОУ, це
1. дуже довга і непотрібна преморедація. Читачі і самі можуть проголосувати за статтю, а неможливісь опубліковати — це відпугує.
2. Нема нормального Q/A, як в StackOverflow чи toster, тобто українского контенту з питаннями та відповідями, що легко індексується — тут не створити
3. проблеми з редактором...

Ну аудіторія маленька.. на іншому ресурсі в мене така ж стаття набирає десятки а то і сотню тисяч переглядів..

tldp.org/...​/html/io-redirection.html
я бы добавил бы это в мастхев помимо команд чтоб понимать что делает 2>&1

особливо, щоб розуміти чим відрізняється «>file.log 2>&1» та «2>&1 > file.log» ? ;)

Корочше я не полінився, додав ще одну секцію у статтю, бо дійсно мастхев.

Дуже хотілося б хоча б один камент на додану секцію "

D) В каментах запропонували розглянути 2>&1

", чи нормально розжував

p.s. це такий чіт для підняття переглядів статті, але цикаво чи дійсно корисно

Bash-скрипти дуже глибоко проникли в *nix і технічно й ідеологічно, вони виросли разом з *nix, а пізніше з Linux та стандартами POSIX.

[зануда ОН]
називати bash-скриптами скріптову мову, яка росла разом із юніксами — перебор. Бо bash з’явився тільки в 1989 році, а його батько, sh від Берні — в 1978-му. Тож, краще все ж термін «shell-скріпти».

Також помилкою є вважати bash найбільш потужним інструментом в лінійці командних процесорів для *nix систем. В тей час, як дійсно є більш прості і легкі sh-based процесори типу dash, широко використовуються і більш потужні командні процесори із своїми діалектами shell-мови, як-то zsh чи ksh. Вже не кажучи про несумісні із bournie shell оболонки csh/tcsh.
[зануда ОФФ]

А стаття цікава :)

Сам такий, тому мені нескладно трохи виправити початок статті. Дякую за зауваження.

Якщо перенаправлення та пайплайни це різні речі, то як же філософія юнікс «все є файлом»?

пайп — це символ вертикальное черти, все є файл — трошки застаріла жартівна фраза, я вважаю що малось на увазы — все є стрім, просто більшість source та destination можуть мати файловый шлях.

Ну і пайплайн це взагалі цепочка виконань різніх завдань, коли кожне наступне отримує результат виконання попереднього — це вже до сучасних ci/cd

$ IFS=";" ; read one two three <<< “с1;с2;с3;с4” ; echo $three
c3;c4

вивело “c3 c4” через пробіл

виправив опечатку, додав ще один приклад, щоб показати що решта значень попаде в останню змінну

Поправив оформлення. Дуже сумно, що редактор на Dou працює з помилками, і редагувати складний текст — це біль.
Але можно зробити отак blabla/edit/?old та попасти в raw редактор, де все простіше.

Сергію, вітаю. Як і обговорювали з вами на пошті, дійсно можна в raw-режим ось так перемкнутись: dou.ua/forums/new/?old, а ось так для редагування: dou.ua/...​ums/topic/51355/edit/?old.

Поки що ца стаття в мене чемпіон по зірочкам. Хоча в довоєнні роки переглядів було б гораздо більше (

Будемо чекати ваші наступні матеріали! І дякуємо, що ви з DOU, а зірочок дійсно багато 🤩

Дякую за статтю. Напишіть про те, коли варто і не варто використовувати баш

Ну так складно сказати.

Для мене робити на баш простіше, коли треба працювати с не дуже організованими даними, і я не можу просто взяти та конвертнути в масив. В шелл скриптах можно використати декілька операцій через awk та конвертнути в потрібний формат. Але в pyton бібліотека дозволяє простіше обробляти помилки.

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

Чи треба щось зробити в init контейнері — підготувати сертифікати чи секрети, встановити права доступу та інше.

Тобто дуже багато дрібного клею, на якому я звик в баш робити речі простіше для себе. Зараз в мене навіть телеграм бот на баш є, з різними штуками, вміє малювати карти, грати в шахи.
Ось картинка на початку статті — це як раз код для консольного bс, який обчислює дистанцію між двома координатами, що можно получити через googlemap api, а так як земля не плоска, то для розрахунків дистанціі між координатами (довгота та широта) треба трохи заморочитись.

Чесно кажучи, очікував чергової статті для початківців з прикладами cd, ls, touch/mkdir, але дуже приємно здивований крутими фішками. Автору — респект. Пиши ще)

Це значно краще ніж «виправлення» хоум брю

Я мабуть не в темі, з чим ви порівняли?

Ту тему особисто я не читав, адже ніколи не працював з macOS, та припускаю, що йдеться про Встановлення Homebrew в macOS

ну да, там неясно чому apt-get не підійшов

там неясно чому apt-get не підійшов

Там насправді все ясно: тому що пакетний менеджер apt не використовується в MacOS, там якраз Homebrew найпопулярніший. А apt-get нехай залишимо дебіаноподібним дистрибутивам.

Підписатись на коментарі