Шпаргалка по Bash скриптам

Основы bash

Скрипты Bash имеют расширение .sh:

$ touch script.sh

Хорошей практикой считается указывать путь до вашего терминала вначале каждого скрипта:

#! /bin/bash

Этот прием называется shebang, подробнее можно почитать тут

Список доступных терминалов в вашей системе можно посмотреть с помощью этой команды:

$ cat /etc/shells

Hello world

#! /bin/bash
echo "Hello world"

Запуск скрипта:

$ bash script.sh

Скрипт можно сделать исполняемым файлом и запускать без команды bash:

$ chmod +x script.sh
$ ./script.sh

Комментарии

Однострочные комментарии:

# Это просто коммент
# И это тоже
echo "Hello from bash" # эта команда выводит строку в консоль

Мультистрочные комментарии:

: 'Мультистрочные комментарии очень удобны
для подробного описания ваших скриптов.
Успользуйте их с умом!'

Переменные

MY_STRING="bash is cool"
echo $MY_STRING # Вывод значения переменной

Имя переменной не должно начинаться с цифры

Пользовательский ввод

Команда read читает пользовательский ввод и записывает его в указанную переменную:

echo "Введите ваше имя:"
read NAME
echo "Привет $NAME!"

Если переменная не указана, то команда read по умолчанию сохранит все данные в переменную REPLY

Можно записывать несколько переменных. Для этого, при вводе из терминала, значения необходимо разделять пробелом:

read V1 V2 V3
echo "1 переменная: $V1"
echo "2 переменная: $V2"
echo "3 переменная: $V3"
$ bash script.sh
$ hello world some other text
1 переменная: hello
2 переменная: world
3 переменная: some other text

Флаг -a позволяет создать массив в который будут записываться строки пользовательского ввода разделенные пробелом:

read -a NAMES
echo "Массив имён: ${NAMES[0]}, ${NAMES[1]}, ${NAMES[2]}"
$ bash script.sh
Alex Mike John
Массив имён: Alex, Mike, John

Флаг -p позволяет не переносить пользовательский ввод на следующую строку.

Флаг -s позволяет скрыть вводимые символы (как это происходит при вводе пароля).

read -p "Введите ваш логин: " LOGIN
read -sp "Введите ваш пароль: " PASSWD
$ bash script.sh
Введите ваш логин: bash_hacker
Введите ваш пароль: 

Передача аргументов

Аргументы это просто значения, которые могут быть указаны при запуске скрипта.

Всем переданным аргументам присваивается уникальное имя равное их порядковому номеру:

echo "Аргумент 1 - $1; aргумент 1 - $2; aргумент 1 - $3."
$ bash script.sh hello test 1337
Аргумент 1 - hello; aргумент 1 - test; aргумент 1 - 1337.

Нулевой аргумент всегда равен названию файла со скриптом:

echo "Вы запустили файл $0"
$ bash script.sh
Вы запустили файл script.sh

Все аргументы можно положить в именованный массив:

args=("$@")
echo "Полученные аргументы: ${args[0]}, ${args[1]}, ${args[2]}."
$ bash script.sh some values 123
Полученные аргументы: some, values, 123

@ - это название массива по умолчанию, который хранит все аргументы (за исключением нулевого)

Количество переданных аргументов (за исключением нулевого) хранится в переменной #:

echo "Всего получено аргументов: $#"

Условия if else

Условия всегда начинаются с ключевого слова if и заканчиваются на fi:

echo "Введите ваш возраст:"
read AGE

if (($AGE >= 18))
then
    echo "Доступ разрешен"
else
    echo "Доступ запрещен"
fi
$ bash script.sh
Введите ваш возраст:
19
Доступ разрешен

$ bash script.sh
Введите ваш возраст:
16
Доступ запрещен

Условий может быть сколько угодно много, для этого используется конструкция elif, которая также как if может проверять условия:

read COMMAND

if [ $COMMAND = "help" ]
then
    echo "Доступные команды:"
    echo "ping - вернет строку PONG"
    echo "version - вернет номер версии программы"
elif [ $COMMAND = "ping" ]
then
    echo "PONG"
elif [ $COMMAND = "version" ]
then
    echo "v1.0.0"
else
    echo "Команда не определена. Воспользуйтесь командой 'help' для справки"
fi

Обратите внимание, что после конструкций if и elif всегда следует строчка с ключевым словом then
Так же не забывайте отделять условия пробелами внутри фигурных скобок -> [ condition ]

Операторы условий

Для цифр и строк могут использоваться разные операторы сравнения. Полные их списки с примерами приведены в таблицах ниже.

Обратите внимания, что разные операторы используются с определенными скобками

Операторы сравнения для чисел

Оператор Описание Пример
-eq равняется ли if [ $age -eq 18 ]
-ne не равняется if [ $age -ne 18 ]
-gt больше чем if [ $age -gt 18 ]
-ge больше чем или равно if [ $age -ge 18 ]
-lt меньше чем if [ $age -lt 18 ]
-le меньше чем или равно if [ $age -le 18 ]
> больше чем if (($age > 18))
< меньше чем if (($age < 18))
=> больше чем или равно if (($age => 18))
<= меньше чем или равно if (($age <= 18))

Операторы сравнения для строк

Оператор Описание Пример
= проверка на равенство if [ $str = "hello" ]
== проверка на равенство if [ $str == "hello" ]
!= проверка на НЕ равенство if [ $str != "hello" ]
< сравнение меньше чем по ASCII коду символов if [[ $str < "hello" ]]
> сравнение больше чем по ASCII коду символов if [[ $str > "hello" ]]
-z проверка пустая ли строка if [ -z $str ]
-n проверка есть ли в строке хоть один символ if [ -n $str ]

Так же существуют операторы для проверки различных условий над файлами.

Операторы для проверки файлов

Оператор Описание Пример
-e проверяет, существует ли файл if [ -e $file ]
-s проверяет, пустой ли файл if [ -s $file ]
-f проверяет, является ли файл обычным файлом, а не каталогом или специальным файлом if [ -f $file ]
-d проверяет, является ли файл каталогом if [ -d $file ]
-r проверяет, доступен ли файл для чтения if [ -r $file ]
-w проверяет, доступен ли файл для записи if [ -w $file ]
-x проверяет, является ли файл исполяемым if [ -x $file ]

Логические операторы

Условия с оператором "И" возвращают истину только в том случае, когда все условия истины.

Существует несколько вариантов написания условий с логическими операторами

if [ $age -ge 18 ] && [ $age -le ]
if [ $age -ge 18 -a $age -le ]
if [[ $age -ge 18 && $age -le ]]

Условия с оператором "ИЛИ" возвращают истину в том случае, когда хотя бы одно условие истинно.

if [ -r $file ] || [ -w $file ]
if [ -r $file -o -w $file ]
if [[ -r $file || -w $file ]]

Арифметические операторы

num1=10
num2=5

# Сложение
echo $((num1 + num2))      # 15
echo $(expr $num1 + $num2) # 15

# Вычитание
echo $((num1 - num2))      # 5
echo $(expr $num1 - $num2) # 5

# Умножение
echo $((num1 * num2))       # 50
echo $(expr $num1 \* $num2) # 50

# Деление
echo $((num1 / num2))      # 2
echo $(expr $num1 / $num2) # 2

# Остаток от деления
echo $((num1 % num2))      # 0
echo $(expr $num1 % $num2) # 0

Обратите внимание, что при использовании умножения с ключевым словом expr необходимо использовать косую черту.

Конструкция switch case

Не всегда удобно использовать конструкции if/elif для большого количества условий. Для этого лучше подойдет конструкция case:

read COMMAND

case $COMMAND in
    "/help" )
        echo "Вы открыли справочное меню" ;;
    "/ping" )
        echo "PONG" ;;
    "/version" )
        echo "Текущая версия: 1.0.0" ;;
    * )
        echo "Такой команды нет :(" ;;
esac

Случай со звездочкой * отработает лишь в том случае, если не подойдет ни одно из условий выше.

Массивы

Массивы позволяют хранить целую коллекцию данных в одной переменной. С этой переменной можно удобно и легко взаимодействовать:

array=('aaa' 'bbb' 'ccc' 'ddd')

echo "Элементы массива: ${array[@]}"
echo "Первый элемент массива: ${array[0]}"
echo "Индексы элементов массива: ${!array[@]}"

array_length=${#array[@]}
echo "Длинна массива: ${array_length}"
echo "Последний элемент массива: ${array[$((array_length - 1))]}"
$ bash script.sh
Элементы массива: aaa bbb ccc ddd
Первый элемент массива: aaa
Индексы элементов массива: 0 1 2 3
Длинна массива: 4
Последний элемент массива: ddd

Обратите внимание, что элементы массива разделются пробелом без запятой.

Элементы массива можно добавлять/перезаписывать/удалять по ходу выполнения скрипта:

array=('a' 'b' 'c')

array[3]='d'
echo ${array[@]} # a b c d

array[0]='x'
echo ${array[@]} # x b c d

array[0]='x'
echo ${array[@]} # x b c d

unset array[2]
echo ${array[@]} # x b d

Цикл while

Цикл while повторяет выполение блока кода описанного между ключевыми словами do - done пока истино заданное условие.

i=0

while (( $i < 5 ))
do
    i=$((i + 1))
    echo "Итерация номер $i"
done
$ bash script.sh
Итерация номер 1
Итерация номер 2
Итерация номер 3
Итерация номер 4
Итерация номер 5

Операция увеличения числа на 1 единицу называется инкриментом и для неё существует специальная запись:

(( i++ )) # post increment
(( ++i )) # pre increment

Противоположная операция - декремент:

(( i-- )) # post decrement
(( --i )) # pre decrement

С помощью while циклов можно построчно читать различные файлы. Существует несколько способов сделать это:

echo "Чтение файла по строкам:"
while read line
do
    echo $line
done < text.txt 
echo "Чтение файла по строкам:"
cat text.txt | while read line
do
    echo $line
done
echo "Чтение файла по строкам:"
while IFS='' read -r line
do
    echo $line
done < text.txt

Цикл until

Цикл until противоположен циклу while тем, что он выполняет блок кода описанный между ключевыми словами do - done тогда, когда заданное условие возвращает false:

i=5
until (( $i == 0 )) # будет выполняться пока i не станет равным 0
do
    echo "Значение переменной i = $i"
    (( i-- ))
done
$ bash script.sh
Значение переменной i = 5
Значение переменной i = 4
Значение переменной i = 3
Значение переменной i = 2
Значение переменной i = 1

Цикл for

Самый классический цикл:

for (( i=1; i<=10; i++ ))
do
    echo $i
done

В новых версиях Bash существует более удобный способ записи с помощью оператора in:

for i in {1..10}
do
    echo $i
done

Условие после ключевого слова in в общем случае выгядит так:

{START..END..INCREMENT}

START - с какого элемента начинать цикл;
END - до какого элемента продолжать цикл;
INCREMENT - на сколько увеличивать элемент после каждой итерации (по умолчанию на 1).

Цикл for можно использовать для последовательного запуска набора команд:

for command in ls pwd date # Список команд для запуска
do
    echo "---Запуск команды $command---"
    $command
    echo "------------------------"
done
$ bash script.sh
---Запуск команды ls---
script.sh  text.txt
------------------------
---Запуск команды pwd---
/home/user/bash
------------------------
---Запуск команды date---
Сб 03 сен 2022 10:35:57 +03
------------------------

Ключевое слово break останавливает выполнение цикла.

Ключевое слово continue завершает текущую итерацию цикла и переходит к следующей.

Цикл select

Крайне удобный цикл для создания меню выбора опций:

select color in "Красный" "Зеленый" "Синий" "Белый"
do
    echo "Вы выбрали $color цвет…"
done
$ bash script.sh
1) Красный
2) Зеленый
3) Синий
4) Белый
#? 1
Вы выбрали Красный цвет…
#? 2
Вы выбрали Зеленый цвет…
#? 3
Вы выбрали Синий цвет…
#? 4
Вы выбрали Белый цвет…

Цикл select очень хорошо сочетается с оператором выбора case. Таким образом можно очень просто создавать интерактивные консольные приложения с большим количеством разветвлений:

echo "---Добро пожаловать в меню---"

select cmd in "Запуск" "Настройки" "О программе" "Выход"
do
    case $cmd in
    "Запуск")
        echo "Программа запущена"
        echo "Введите число:"
        read input
        echo "$input в квадрате = $(( input * input ))" ;;
    "Настройки")
        echo "Настройки программы" ;;
    "О программе")
        echo "Версия 1.0.0" ;;
    "Выход")
        echo "Выход из программы…"
        break ;;
    esac
done

Break и continue в циклах

Для принудительного выхода из цикла используется ключевое слово break:

count=1

while (($count)) # всегда возвращает истину
do
    if (($count > 10))
    then
        break # принудительный выход несмотря на условие после while
    else
        echo $count
        ((count++))
    fi
done

Для того, чтобы пропустить выполнение текущей итерации в цикле и перейти к следующей - используется ключевое слово continue:

for (( i=5; i>0; i-- ))
do
    if ((i % 2 == 0))
    then
        continue
    fi

    echo $i
done
$ bash script.sh
5
3
1

Функции

Функции - это именованные участки кода, которые могут переиспользоваться неограниченное количество раз:

hello() {
    echo "Hello World!"
}

# вызываем функцию 3 раза:
hello
hello
hello
$ bash script.sh
Hello World!
Hello World!
Hello World!

Функции, так же, как и сами скрипты, могут принимать аргументы. Они имеют такие же названия, но аргументы функций видны только внутри функции, в которую они были переданы:

echo "$1" # аргумент переданный при запуске скрипта

calc () {
    echo "$1 + $2 = $(($1 + $2))"
}

# передача двух аргументов в функцию calc
calc 42 17
$ bash script.sh hello
hello
42 + 17 = 59

Локальные переменные

Если мы объявим какую-либо переменную, а затем объявим ещё одну с таким же именем, но уже внутри функции, то у нас произойдет перезапись:

VALUE="hello"

test() {
    VALUE="linux"
}

test
echo $VALUE
$ bash script.sh
linux

Чтобы предотвратить такое поведение используются ключевое слово local перед именем переменной, которая объявляется внутри функции:

VALUE="hello"

test() {
    local VALUE="linux"
    echo "Переменная внутри функции: $VALUE"
}

test
echo "Глобальная переменная: $VALUE"
$ bash script.sh
Переменная внутри функции: linux
Глобальная переменная: hello

Ключевое слово readonly

По умолчанию, каждая созданная переменная в Bash в последующем может перезаписываться. Чтобы защитить переменную от изменений можно использовать ключевое слово readonly:

readonly PI=3.14
PI=100

echo "PI = $PI"
$ bash script.sh
script.sh: строка 2: PI: переменная только для чтения
PI = 3.14

readonly можно использовать не только в момент объявления переменной, но и после:

VALUE=123
VALUE=$(($VALUE * 1000))
readonly VALUE
VALUE=555

echo $VALUE
$ bash script.sh
script.sh: строка 4: VALUE: переменная только для чтения
123000

Тоже самое касается функций. Они так же могут быть переопределены, поэтому их можно защитить с помощью readonly указав при этом флаг -f:

test() {
    echo "This is test function"
}

readonly -f test

test() {
    echo "Hello World!"
}

test
$ bash script.sh
script.sh: строка 9: test: значение функции можно только считать
This is test function

Обработка сигналов

Во время выполнения скриптов, могут происходить неожиданные действия. Например, пользователь может прервать выполнения скрипта с помощь комбинации Ctrl + C, либо может случайно закрыть терминал или в самом скрипте может случится какая-либо ошибка и так далее…

В POSIX-системах существуют специальные сигналы - уведомления процесса о каком-либо событии. Их список определен в таблице ниже:

Сигнал Код Действие Описание
SIGHUP 1 Завершение Закрытие терминала
SIGINT 2 Завершение Сигнал прерывания (Ctrl-C) с терминала
SIGQUIT 3 Завершение с дампом памяти Сигнал «Quit» с терминала (Ctrl-)
SIGILL 4 Завершение с дампом памяти Недопустимая инструкция процессора
SIGABRT 6 Завершение с дампом памяти Сигнал, посылаемый функцией abort()
SIGFPE 8 Завершение с дампом памяти Ошибочная арифметическая операция
SIGKILL 9 Завершение Процесс уничтожен (kill signal)
SIGSEGV 11 Завершение с дампом памяти Нарушение при обращении в память
SIGPIPE 13 Завершение Запись в разорванное соединение (пайп, сокет)
SIGALRM 14 Завершение Сигнал истечения времени, заданного alarm()
SIGTERM 15 Завершение Сигнал завершения (сигнал по умолчанию для утилиты kill)
SIGUSR1 30/10/16 Завершение Пользовательский сигнал № 1
SIGUSR2 31/12/17 Завершение Пользовательский сигнал № 2
SIGCHLD 20/17/18 Игнорируется Дочерний процесс завершен или остановлен
SIGCONT 19/18/25 Продолжить выполнение Продолжить выполнение ранее остановленного процесса
SIGSTOP 17/19/23 Остановка процесса Остановка выполнения процесса
SIGTSTP 18/20/24 Остановка процесса Сигнал остановки с терминала (Ctrl-Z)
SIGTTIN 21/21/26 Остановка процесса Попытка чтения с терминала фоновым процессом
SIGTTOU 22/22/27 Остановка процесса Попытка записи на терминал фоновым процессом

В Bash есть ключевое слово trap с помощью которого можно отлавливать различные сигналы и предусматривать выполнение определенных команд:

trap <КОМАНДА> <СИГНАЛ>

Под сигналом можно использовать его название (колонка Сигнал в таблице), либо его код (колонка Код в таблице). Можно указывать несколько сигналов разделяя их названия или коды пробелом.
Исключения: сигналы SIGKILL (9) и SIGSTOP (17/19/23) отловить невозможно, поэтому нет смысла их указывать.

trap "echo Выполнение программы прервано…; exit" SIGINT

for i in {1..10}
do
    sleep 1
    echo $i
done
$ bash script.sh
1
2
3
4
^CВыполнение программы прервано…
Интересные статьи