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

<div align="right"><a href="https://github.com/cheatsnake/backend-cheats#%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D1%8B-bash">Вернуться на главную страницу ⬆️</a></div>

**Содержание:**

- [Hello world](#hello-world)
- [Комментарии](#комментарии)
- [Переменные](#переменные)
- [Пользовательский ввод](#пользовательский-ввод)
- [Передача аргументов](#передача-аргументов)
- [Условия if else](#условия-if-else)
- [Операторы условий](#операторы-условий)
- [Логические операторы](#логические-операторы)
- [Арифметические операторы](#арифметические-операторы)
- [Конструкция switch case](#конструкция-switch-case)
- [Массивы](#массивы)
- [Цикл while](#цикл-while)
- [Цикл until](#цикл-until)
- [Цикл for](#цикл-for)
- [Цикл select](#цикл-select)
- [Break и continue в циклах](#break-и-continue-в-циклах)
- [Функции](#функции)
- [Локальные переменные](#локальные-переменные)
- [Ключевое слово readonly](#ключевое-слово-readonly)
- [Обработка сигналов](#обработка-сигналов)

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

Хорошей практикой считается указывать путь до вашего терминала вначале каждого скрипта:
```sh
#! /bin/bash
```
> Этот прием называется **shebang**, подробнее можно почитать [тут](https://ru.wikipedia.org/wiki/%D0%A8%D0%B5%D0%B1%D0%B0%D0%BD%D0%B3_(Unix))

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

## Hello world

```sh
#! /bin/bash
echo "Hello world"
```

Запуск скрипта:
```
$ bash script.sh
```

Скрипт можно сделать исполняемым файлом и запускать без команды `bash`:
```
$ chmod +x script.sh
```
```
$ ./script.sh
```

## Комментарии

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

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

## Переменные

```sh
MY_STRING="bash is cool"
echo $MY_STRING # Вывод значения переменной
```
> Имя переменной не должно начинаться с цифры

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

Команда `read` читает пользовательский ввод и записывает его в указанную переменную:
```sh
echo "Введите ваше имя:"
read NAME
echo "Привет $NAME!"
```
> Если переменная не указана, то команда `read` по умолчанию сохранит все данные в переменную `REPLY`


Можно записывать несколько переменных. Для этого, при вводе из терминала, значения необходимо разделять пробелом:
```sh
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` позволяет создать массив в который будут записываться строки пользовательского ввода разделенные пробелом:
```sh
read -a NAMES
echo "Массив имён: ${NAMES[0]}, ${NAMES[1]}, ${NAMES[2]}"
```
```
$ bash script.sh
Alex Mike John
Массив имён: Alex, Mike, John
```

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

Флаг `-s` позволяет скрыть вводимые символы (как это происходит при вводе пароля).
```sh
read -p "Введите ваш логин: " LOGIN
read -sp "Введите ваш пароль: " PASSWD
```
```
$ bash script.sh
Введите ваш логин: bash_hacker
Введите ваш пароль: 
```

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

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

Всем переданным аргументам присваивается уникальное имя равное их порядковому номеру:
```sh
echo "Аргумент 1 - $1; aргумент 1 - $2; aргумент 1 - $3."
```
```
$ bash script.sh hello test 1337
Аргумент 1 - hello; aргумент 1 - test; aргумент 1 - 1337.
```

Нулевой аргумент всегда равен названию файла со скриптом:
```sh
echo "Вы запустили файл $0"
```
```
$ bash script.sh
Вы запустили файл script.sh
```

Все аргументы можно положить в именованный массив:
```sh
args=("$@")
echo "Полученные аргументы: ${args[0]}, ${args[1]}, ${args[2]}."
```
```
$ bash script.sh some values 123
Полученные аргументы: some, values, 123
```
> `@` - это название массива по умолчанию, который хранит все аргументы (за исключением нулевого)

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

## Условия if else

Условия всегда начинаются с ключевого слова `if` и заканчиваются на `fi`:
```sh
echo "Введите ваш возраст:"
read AGE

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

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

Условий может быть сколько угодно много, для этого используется конструкция `elif`, которая также как `if` может проверять условия:
```sh
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` <br>
> Так же не забывайте отделять условия пробелами внутри фигурных скобок -> `[ 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 ] |

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

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

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

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

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

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

```bash
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:

```sh
read COMMAND

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

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

## Массивы

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

```sh
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
```
> Обратите внимание, что элементы массива разделются пробелом без запятой.

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

```sh
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` пока истино заданное условие.

```sh
i=0

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

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

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

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

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

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

```sh
echo "Чтение файла по строкам:"
while read line
do
	echo $line
done < text.txt	
```

```sh
echo "Чтение файла по строкам:"
cat text.txt | while read line
do
	echo $line
done
```

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

## Цикл until

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

```sh
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

Самый классический цикл:
```sh
for (( i=1; i<=10; i++ ))
do
	echo $i
done
```

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

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

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

```
{START..END..INCREMENT}
```
> _START_ - с какого элемента начинать цикл; <br> 
> _END_ - до какого элемента продолжать цикл; <br> 
> _INCREMENT_ - на сколько увеличивать элемент после каждой итерации (по умолчанию на 1).


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

```sh
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` завершает текущую итерацию цикла и переходит к следующей. <br>

## Цикл select

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

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

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

```sh
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`:

```sh
count=1

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

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

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

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

## Функции

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

```sh
hello() {
	echo "Hello World!"
}

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

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

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

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

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

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

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

```sh
VALUE="hello"

test() {
	VALUE="linux"
}

test
echo $VALUE
```
```
$ bash script.sh
linux
```

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

```sh
VALUE="hello"

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

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

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

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

```sh
readonly PI=3.14
PI=100

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

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

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

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

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

```sh
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 <КОМАНДА> <СИГНАЛ>
```
> Под сигналом можно использовать его название (колонка _Сигнал_ в таблице), либо его код (колонка _Код_ в таблице). Можно указывать несколько сигналов разделяя их названия или коды пробелом. <br>
> **Исключения:** сигналы SIGKILL (9) и SIGSTOP (17/19/23) отловить невозможно, поэтому нет смысла их указывать.

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

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

## Отладка скриптов

Запуск скрипта с параметром `-x` покажет его поэтапное выполнение, что будет полезно при отладке и поиске ошибок:

```
$ bash -x script.sh
```

<div align="right"><a href="https://github.com/cheatsnake/backend-cheats/blob/master/files/linux/bash-scripts-cheatsheet.md#%D1%88%D0%BF%D0%B0%D1%80%D0%B3%D0%B0%D0%BB%D0%BA%D0%B0-%D0%BF%D0%BE-bash-%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%B0%D0%BC">Вернуться в начало ⬆️</a></div>