Shell Scripting
Shell, або оболонка командного рядка — це загальна назва програм, які зчитують команди з клавіатури, виконують їх, і відображають вивід на екран. До винайдення графічного інтерфейсу (Graphic User Interface), Command Line Interface з різними оболонками командного рядка були єдиним способом взаємодіяти з компʼютером.
Оболонки командного рядка мають ряд переваг перед графічним інтерфейсом.
- Вони споживають набагато менше обчислювальних ресурсів компʼютера ніж графічна оболонка операційної системи.
- Програмувати для оболонки командного рядка набагато легше ніж для графічного інтерфейсу.
- Графічний інтерфейс програм важко, а іноді практично неможливо переносити на різні операційні системи, наприклад з Linux на Windows.
- Операції в оболонці командного рядка можна легко автоматизовувати, тобто писати скрипти.
На даний момент існує багато різних оболонок командного рядка: - bourne shell, або sh — найстаріша оболонка командного рядка. Усі Unix-подібні операційні системи мають цю, або повністю сумісну з нею програму оболонки командного рядка, навіть якщо вона не використовується ніким із користувачів комп'ютера.
-
bash (акронім від Bourne Again SHell) — покращена оболонка командного рядка, написана для GNU Project, як безплатна заміна sh. bash використовується у всіх дистрибутивах Linux, вона також встановлена за замовчуванням в macos і її можна встановити на windows.
-
Zsh — частково сумісна з bash оболонка командного рядка, яка у macOS використовується за замовчуванням. Вона створювалась як альтернатива bash та більше націлена на інтерактивне використання термінала ніж на скриптинг.
-
cmd і PowerShell — оболонки командного рядка Windows. cmd — старша і простіша, а PowerShell — нова, написана з нуля, та зі значно розширеними можливостями як для інтерактивного користування, так і для автоматизації.
Команди та синтаксис кожної з оболонок відрізняються. Оскільки ми вивчаємо операційні системи на прикладі Linux, далі ми сфокусуємось саме на роботі з bash.
Скрипт на bash
Створимо простий bash-скрипт, який виведе повідомлення в термінал. Це можна зробити в будь-якому текстовому редакторі:
💡 Суфікс .sh у кінці файлу не обовʼязковий, але його заведено додавати. Так легше відрізнити bash-скрипт від будь-якого іншого типу файлу.
Додай перший рядок до файлу:
Цей рядок називається sharp exclamation, або скорочено shebang. Решітка # в комбінації зі знаком оклику — це спеціальна комбінація символів, яка в Unix-подібних операційних системах позначає інтерпретатор командного рядка, який повинен виконати цей скрипт, у нашому випадку це bash. Також зверни увагу на те, що ми додали повний шлях до файлу /bin/bash, він однаковий незалежно від дистрибутиву Linux. На місці bash може бути будь-який інтерпретатор: sh, Zsh і навіть Python.
Якщо ти не вказуватимеш shebang, операційна система намагатиметься виконувати скрипти з тією оболонкою, з якої здійснюється його виклик. Наприклад, якщо ти пишеш bash-скрипт і спробуєш запустити його на macOS, то він буде інтерпретуватись як Zsh-скрипт. Через це скрипт може працювати некоректно. Тож завжди вказуй shebang.
Після shebang можна писати код — послідовність потрібних команд. Додамо в наш скрипт команду echo, яка в stdout напише те, що ти їй передамо (аналог функції print в Python):
Збережемо та виконаємо скрипт:
Виконувати скрипти набагато зручніше змінивши права на файл. Права за замовчуванням дозволяють лише читати та записувати цей файл. Перевіримо:
Щоб дозволити виконання скрипта як програми, потрібно виконати одну з наступних команд:
Друга команда додає права на виконання для всіх типів користувачів, але не зачіпає вже встановлені права на запис та читання (на відмінну від першої). Тепер права дозволяють виконувати скрипт як програму. Такі файли у виводі команди ls -l позначаються зеленим кольором.
Виконаймо наш скрипт. Зверни увагу, що для того, щоб це зробити, треба вказати саме шлях до файлу (можна навіть відносний), а не тільки імʼя:
Вітаємо! Перший shell-скрипт на bash готовий.
Змінні
bash підтримує змінні. Ось приклад оголошення змінних:
Якщо значення змінної — це рядок, який містить пробіли, то одинарні чи подвійні лапки обовʼязкові.
Щоб використати змінну, треба додати до її імені $:
У bash є можливість підставляти значення змінних у рядки:
Така підстановка називається string substitution. Вона відбувається коли ти пишеш рядки в подвійних лапках, в одинарних вона не спрацює.
💡 Між подвійними лапками може бути текст, змінні, або і те, і інше (актуально при використанні користувацьких змінних та змінних оболонки у скриптах), а вміст між одинарними лапками завжди має незмінний характер.
Як і в будь-якій мові програмування, ти можеш використовувати значення одних змінних, щоб отримувати значення інших:
bash також підтримує арифметичні операції, але в них специфічний синтаксис. Саму арифметичну операцію потрібно писати в подвійних круглих дужках, а змінні потрібно вказувати без $:
Результат попередньої операції нікуди не збережеться, і навіть не виведеться в консоль. Щоб мати можливість щось із ним зробити, треба додати $ перед дужками, тоді ми зможемо зберегти його в іншу змінну або передати як параметр іншій команді:
Окрім звичайних змінних існують ще декілька груп змінних, які можна використовувати. Перша група — аргументи скрипту. Коли ти виконуєш скрипт, ти можеш через пробіл вказувати йому довільну кількість аргументів:
Ти можеш посилатись на ці аргументи, використовуючи спеціальні змінні:
#! /bin/bash
echo "referring arguments based on their position: "
echo "first argument: $1, second argument: $2"
echo "total number of arguments: $#"
echo "all script arguments: $@"
Зчитувати кожен аргумент можна змінною, імʼя якої — це порядковий номер аргументу. Загальна кількість аргументів зберігається у змінній, яка позначається #, а всі аргументи за раз також можна зчитати зі змінної, яка позначається @. Виконаємо цей скрипт і подивимось як він працює:
В bash є також ряд додаткових службових змінних, значення яких ми можемо використовувати в скриптах:
#! /bin/bash
echo "The exit status of the last process to run: $?"
echo "The Process ID (PID) of the current script: $$"
echo "The number of seconds the script has been running for: $SECONDS"
echo "A random number: $RANDOM"
echo "The current line number of the script: $LINENO"
echo "The hostname of the computer running the script: $HOSTNAME"
echo "The username of the user executing the script: $USER"
Змінні середовища
Змінні середовища (environment variables) - це певні значення, які визначаються в операційній системі та використовуються для конфігурації робочого оточення. Кожен процес має свій набір змінних середовища. Коли один процес створює інший, дочірній процес отримує копію змінних середовища батьківського процесу. Окрім цього, операційна система дозволяє користувачу самостійно додавати або перевизначати змінні середовища для процесів.
Подивитись список усіх змінних середовища для поточного процесу можна за допомогою такої команди:
Для того, щоб створити змінну середовища, потрібно скористатись командою export:
Якщо ти напишеш скрипт, який створює змінні середовища, і виконаєш його, то ці змінні встановляться тільки для процесу, у якому виконувався скрипт.
Щоб по завершенню процесу виконання скрипту скопіювати всі його environment variables в поточний процес bash, який є батьківським відносно нього, треба скористатись командою source:
Ця команда має спрощений синтаксис:
Змінна, створена за допомогою export, буде збережена лише для поточного процесу bash. Створити постійну змінну середовища можна кількома способами залежно від того, на якому рівні це потрібно зробити. Якщо ти хочеш, щоб змінна створювалась для твого користувача кожного разу, коли ти запускаєш bash, то її потрібно додати в кінець скрипту .bashrc у форматі export envFromBashRc=”testvalue”:
.bashrc — це скрипт у твоїй домашній директорії, який виконується автоматично кожного разу коли ти запускаєш bash. Щоб побачити цю змінну, тобі потрібно або перезапустити bash, або перезапустити сам скрипт:
Якщо потрібно додати змінну для всіх користувачів, то потрібно використати глобальний скрипт:
І останнє місце, де можна додавати змінні середовища — це глобальний конфігураційний файл. Змінні із цього файлу використовуються для всіх процесів:
💡 Якщо ти вносиш зміни у файл /etc/environment, то для того, щоб їх побачити, перезавантаж компʼютер.
Файл /etc/environment містить змінну PATH, яка містить список усіх тек, у яких операційна система шукає файли програм. Наприклад, коли ти виконуєш cat:
Оболонка командного рядка виділяє cat як імʼя програми (просто тому, що воно стоїть попереду), а все що після нього — зберігає як аргументи програми. Після цього просить операційну систему виконати програму cat із заданими аргументами, а операційна система шукає файл програми у всіх директоріях, які є в змінній PATH.
Змінні середовища зручно використовувати як для конфігурації shell scripts, так і для конфігурації будь-яких програм. Їх дуже легко читати, до того ж вони працюють однаково в Linux, macOS та Windows.
Корисні програми
Порахувати кількість рядків чи слів, знайти в тексті слово, здійснити запит до вебсервера чи завантажити файл — для цього всього є готові вбудовані програми. Знаючи їх, на bash можна написати скрипт, який зробить практично будь-що.
Перша програма — wc, яка призначена для аналізу тексту:
Ця програма вміє рахувати кількість байт, символів, слів чи рядків тексту, а сам текст вона приймає в stdin. Якщо не передавати ніяких додаткових параметрів, то вона порахує все вище перераховане, а якщо потрібно порахувати лише, наприклад, кількість рядків, то можна передати їй відповідний параметр:
Закрити ввід команди можна за допомогою комбінації клавіш Ctrl + D.
сat — інша корисна програма. Якщо викликати її без параметрів, вона виведе свій stdin в stdout. Якщо їй передати імʼя файлу, тоді в stdoutвона виведе вміст файлу. сat зручно використовувати для перегляду маленьких файлів:
Команда grep використовується для пошуку слів у тексті. Першим його параметром треба вказати слово, яке потрібно знайти, а другим — файл чи файли, у яких потрібно знайти дане слово. grep вміє шукати як у файлах, так і з тексту, який йому надсилається в stdin:
Ще одна класна програма, про яку корисно знати при написанні скриптів — curl, за допомогою якої можна здійснювати http-запити:
З curl ти можеш кастомізувати запити до вебсервісів як тобі заманеться: міняти http-метод, використовувати потрібні http headers тощо. Ти також можеш переглядати повну інформацію про відповідь від вебсервісу за допомогою параметру -v (verbose):
Якщо тобі потрібно завантажити якийсь файл, це можна зробити за допомогою wget. Ця команда має багато можливостей для кастомізації запиту, як і curl. Потрібно лише передати імʼя файлу, у який wgetзбереже те, що завантажує:
Перенаправлення потоків вводу/виводу
У Linux-процесів є три потоки вводу/виводу (IO streams): stdin, stdoutта stderr. Почнемо з stdout. В bash ти можеш його перенаправляти за допомогою символу >:
Ліворуч від нього має бути команда, результат виконання якої ти хочеш обробити, а праворуч — ім'я файлу, куди треба зберегти цей результат. Якщо для перенаправлення потоку ти скористаєшся лише одним символом >, то файл кожного разу створюватиметься заново:
Щоб уникнути перестворення файлу, а просто додавати вивід у кінець, треба скористатись двома символами >:
Якщо ми виконаємо якусь неіснуючу команду, термінал виведе помилку:
Якщо ми спробуємо перенаправити вивід команди у файл, ця помилка не збережеться:
Символ > перенаправляє лише stdout, а помилки пишуться у потік stderr. Його можна перенаправити в файл ось так:
stderr ніяк не конфліктує stdout. За потреби ми можемо перенаправляти їх в окремі файли:
В Linux є спеціальний файл, який приймає перенаправлені потоки виводу, але нікуди їх не записує — /dev/null. Він може бути корисним, якщо ти не хочеш зберігати чи виводити логи програми:
Pipe
Іноді може виникнути необхідність надіслати вивід однієї програми як параметр іншій. Наприклад, тобі треба знайти у виводі програми ps усі процеси bash за допомогою grep. Звісно, спочатку можна виконати першу команду, зберегти її вивід у файл, а потім здійснити пошук по цьому файлу:
Але існує набагато ефективніший спосіб, який називається pipe. Ось як використання pipe виглядає на практиці:
Вертикальна риска між командами ps і grep і є pipe. Завдяки йому ми змогли виконати команду ps і передати її вивід як вхідний параметр для команди grep, яка знайшла нам всі процеси bash.
Розглянемо pipe детальніше та запустимо скрипт process-demo.py на Python:
import time
# delay so we will have enough time to start the program
time.sleep(30)
# do some calculations
sum = 0
for x in range(20000):
sum = sum + x
print(sum)
# emulate waiting for an external event
time.sleep(300)
Цей скрипт зробить якісь обчислення та запише їх результат у stdout. Тепер поєднаємо цей скрипт із програмою cat, яка зчитає stdin та збереже його у файл за допомогою перенаправлення. Для того, щоб ми легше могли знайти процеси, створені для цих команд, запустимо їх у backgound. А щоб легше побачити, які саме процеси запустились, перевіримо їх список перед виконанням команди:
Тут зʼявились два окремих процеси: один відповідає нашому Python-скрипту, а інший — програмі сat. Pipe поєднує ці процеси перенаправляючи stdout-потік першого процесу в stdin-потік другого процесу. Другий процес буде знаходитись у заблокованому стані поки перший не передасть дані. Усе, що передається першим, стає вводом для другого процеса. Якщо перший процес завершить свою роботу, то і другий процес, який був запущений через pipe, також завершить свою роботу.
Розглянемо ще один приклад та порахуємо кількість процесів за допомогою wc:
Вивід першої команди показує, що процесів усього 4, хоча друга покаже що процесів всього 2. Річ у тім, що wc -l рахує кількість рядків, які потрапили в stdin, а ps ще включає верхній рядок зі стовпчиками та один додатковий пустий рядок у кінці. Тобто для того, щоб дізнатись реальну кількість процесів, отриману саме таким способом, нам потрібно від кількості процесів відняти 2.
💡 Pipe — це не магія, а просто перенаправлення потоку виводу (stdout) однієї програми в потік вводу (stdin) іншої. Потрібно бути дуже акуратним(-ою) при використанні pipe в bash-скриптах.
Умови та цикли
bash підтримує умовний оператор if. Його синтаксис виглядає ось так:
Умова — вираз у квадратних дужках. Ось деякі умови, які можуть тобі знадобитись:
string="value"
# string conditions:
[ "$string" = "value" ] # true if variable is equal to some value
[ "$string" != "anothervalue" ] # true if variable is not equal to some value
[ -z "$string" ] # true if string is empty
[ -n "$string" ] # true if string is not empty
Зверни увагу, що всі змінні знаходяться в подвійних лапках, саме так порівнюються рядки в bash. Щоб перевірити, що рядок пустий, треба скористатись оператором -z, а для того, щоб перевірити, що рядок не пустий, треба скористатись оператором -n. Також зверни увагу на пробіли: якщо при оголошенні змінної в bash ми не розділяємо пробілами ім'я змінної, знак дорівнює, і значення, то в умовах — обов'язковорозділяємо змінні, значення, оператори та дужки.
У bash можна використовувати й цикли, наприклад:
#! /bin/bash
counter=1
while [ $counter -le 10 ]
do
echo $counter
((counter++))
done
echo "All done"
Умова циклу оголошується точно так ще як і для умовного оператора if, а тіло циклу обмежується ключовими словами do і done, між якими знаходяться команди, які потрібно виконати.
Тепер трохи попрактикуємось та напишемо скрипт, який перевіряє, хто користувався програмою sudo протягом останніх пари днів. Логи sudoзберігаються у файлі /var/log/auth.log, виконаймо якусь операцію з ним та перевіримо вміст файлу:
Цей лог містить багато різних записів, і лише один із них цікавить нас — той, де є sudo і власне команда. Створимо новий скрипт (sudo-monitor.sh), який читає цей лог-файл та виводить нам усі такі рядки:
#! /bin/bash
log_file="/var/log/auth.log"
echo “Checking the log for sudo usages”
cat $log_file | while read line
do
sudoLogRecord=$(echo "$line" | grep "sudo" | grep "TTY")
if [ -n "$sudoLogRecord" ]
then
echo "$sudoLogRecord"
fi
done
Подивимось цей скрипт детальніше:
/bin/bash— це shebang.log_file="/var/log/auth.log"— це ім’я файлу з логом, яке ми збережемо в змінній.echo “Checking the log for sudo usages”— це команда для логування скрипту, так легше буде розуміти що він робить, коли ми його запускатимемо.cat $log_file | while read line— це команда, за допомогою якої ми отримаємо вміст файлу, а програмаreadзчитує лише один рядок зstdinта зберігає його в змінну, а якщо її використати в умові циклу, якому вміст усього файлу передається через pipe, то цикл пройде по всім рядкам.sudoLogRecord=$(echo "$line" | grep "sudo" | grep "TTY")— у середині тіла циклу ми можемо знайти потрібний нам рядок за допомогоюgrep. Якщо$lineне міститьsudo, то результатом виконанняgrepбуде пустий рядок. Якщо ми повернемось до логу, то команди можна відфільтрувати за наявністюTTY. Результат фільтрування ми збережемо в зміннуsudoLogRecord. Якщо цей рядок має потрібні нам слова (sudoіTTY), то ця змінна міститиме потрібний рядок, а якщо не має, то рядок буде пустим.if [ -n "$sudoLogRecord" ]— це перевірка, чи рядок є пустим; якщо так, то ми виведемо цей рядок у лог нашого скрипту (echo "$sudoLogRecord").- -n string is not null. -z string is null, that is, has zero length