# Сценарии bash

В этом разделе мы научимся оформлять сценарии `bash` -- исполняемые файлы, содержащие сразу несколько команд.
Для этого мы познакомимся с кодами возврата программ, командой проверки условий `test`, условным оператором `if-then-else-fi`, механизмом запуска сценариев.
Часть информации, которая поможет нам, была представлена ранее.
Так, для ввода и вывода данных воспользуемся командами `read` и `echo` из раздела ["Стандартные потоки"](./streams.md).
Переменные `bash` отличаются от [переменных среды](./env.md) только тем, что вводятся без команды `export`.


## Введение

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

Файлам со сценариями `bash` (и других командных оболочек) принято давать расширение `.sh`.

Ниже показан пример кода из файла со сценарием `/etc/profile.d/gnome-session_gnomerc.sh`.

```bash
# shellcheck shell=sh

# If we are running the GNOME session, source ~/.gnomerc, but only if
# we're in Wayland mode. If we're in X11 mode, this will be done by
# /etc/X11/Xsession.d/55gnome-session_gnomerc instead.

if [ "${XDG_SESSION_TYPE-}" = wayland ]; then
  desktops="$(IFS=:; set -- $XDG_CURRENT_DESKTOP; echo "$@")"
  for desktop in $desktops; do
    [ "$desktop" = "GNOME" ] && break
  done
  unset desktops

  if [ "$desktop" != "GNOME" ]; then
    unset desktop
    return
  fi
  unset desktop

  GNOMERC="$HOME/.gnomerc"
  if [ -r "$GNOMERC" ]; then
    # shellcheck source=/dev/null
    . "$GNOMERC"
  fi
fi

# We prepend /usr/share/gnome since its defaults.list actually points
# to /etc so it is configurable. This is idempotent, so we can do this
# unconditionally.
if [ -z "$XDG_DATA_DIRS" ]; then
  XDG_DATA_DIRS=/usr/share/gnome:/usr/local/share/:/usr/share/
elif [ -n "${XDG_DATA_DIRS##*/usr/share/gnome*}" ]; then
  XDG_DATA_DIRS=/usr/share/gnome:"$XDG_DATA_DIRS"
fi
export XDG_DATA_DIRS
```

Те строки, что начинаются с символа `#` относятся к комментариям.
Кроме знакомых команд `export`, `unset` и манипулирования переменными можно увидеть управляющие конструкции как оператор ветвления (`if-then-fi`) и цикла (`for-do-done`).
Управляющие конструкции превращают `bash` в полноценный язык программирования.

```{note}
Если сценарий состоит из нескольких коротких команд, то их можно объединить в одну строку, разделив символом `;`.
Так, создание каталога `mydir` и переход в него можно выполнить командой `mkdir mydir; cd mydir`.
Символ `;` подсказывает интерпретатору, что ввод аргументов команды завершается и за ним идет следующая команда.
Чтобы `;` стал частью аргумента команды, то этот аргумент следует обрамить кавычками.
Так, команда `mkdir "mydir;"` создаст каталог `mydir;`.
```


## Простой сценарий

Напишем простой сценарий добавления в домашнем каталоге каталога `mydir` и создания в нем трех файлов `a.txt`, `b.txt` и `c.txt`, содержащие собственные названия.
Оформим сценарий в полноценный исполняемый файл и запустим его.
Для этого мы должны:
1) создать файл `myscript.sh`;
2) добавить в начало файла строку с **шебангом**;
3) добавить команды, составляющую логику сценария;
4) включить атрибут файла "исполняемый";
5) запустить файл;
6) проверить, что задача выполнена сценарием правильно.

Первые три пункта выполним в текстовом редакторе `vim`.
Создайте пустой файл и добавьте первой строкой
``` bash
#!/bin/bash
```
В строках ниже добавьте команды:
``` bash
mkdir mydir
echo a.txt > mydir/a.txt
echo b.txt > mydir/b.txt
echo c.txt > mydir/c.txt
```
Сохраните файл и выйдите из редактора.

<!-- Исполняемый файл и превращение в него -->
В системе `Windows` исполняемость указывает расширение файла: `.exe`, `.dll`, `.com`.
В системах `UNIX` признак исполняемости задается атрибутом файла.
Команда `ls` выделяет исполняемые файлы зеленым цветом.
Созданный в текстовом редакторе файл по умолчанию не является исполняемым.
Команда `chmod +x file.sh` дает файлу `file.sh` такое свойство.

```{image} images/chmod-executable-1.png
:align: center
```

```{note}
Команда `chmod -x file.sh` сбрасывает атрибут исполняемого файла.
```

<!-- Запуск исполняемого файла -->
Исполняемый файл можно запустить по имени только в том случае, если каталог, в котором он находится, входит в список переменной окружения `PATH`.
Каталоги из `PATH` обычно являются системными и защищены от модификации.
Создаваемая пользователем программа часто располагается в домашнем или во вложенном в него каталогах.
Есть два способа запустить такую программу:
1) дополнить переменную PATH новым каталогом и запустить программу по имени;
2) запустить файл по полному или относительному имени.

Короткий способ сослаться на файл в текущем каталоге -- это воспользоваться обозначением текущего каталога и именем запускаемого файла:
``` bash
./myscript.sh
```

После запуска проверьте, что сценарий выполнил свою работу.
Загляните в домашний каталог и проверьте, появился ли подкаталог `mydir` с тремя файлами и содержат ли эти файлы нужный нам контент:
```
bob@pc:~$ ls mydir/
a.txt  b.txt  c.txt
bob@pc:~$ cat mydir/*.txt
a.txt
b.txt
c.txt
```


## Подробнее про шебанг

Строка с шебангом `#!/bin/bash` подсказывает системе с каким интерпретатором следует ей исполнить текстовый файл.
В данном случае наш сценарий передается интерпретатору командной строки `bash`, которая расположена в каталоге `/bin`.
Если этой строки не было (и файл не был исполняемым), нам бы пришлось явно указывать интерпретатор:
``` bash
/bin/bash myscript.sh
```

Интерпретатором сценария может быть и `python3`, `perl`, `php`, `ruby`.
Благодаря шебангу текстовый файл можно превратить в программу, запускаемую по имени или двойному щелчку мыши из среды рабочего стола.

После шебанга указан путь к программе-интерпретатору, возможно с опциями.
Например, `#!/bin/bash -e` указывает, что выполнение сценария `bash` следует завершить сразу же при первой ошибке.

<!-- Автоматический выбор программы для открытия файла -->
В среде рабочего стола файл с документом глобально связывается с обрабатываемой его программой благодаря ассоциации расширения файла с исполняемым файлом.
Шебанг дает другой механизм связывания, где выбор делает создатель файла.

```{note}
Не рекомендуется использовать кодировки UNICODE (utf-8, utf-16, utf-32) с указанием маркера последовательности байт (BOM).
В этом случае в файле первые три байта будут равны `EF BB BF`, подсказывающие текстовому редактору кодировку и порядок байт.
Системный загрузчик не увидит шебанг и не сможет выбрать интерпретатор.
```


## Коды завершения

Код завершения -- это число от 0 до 255, которое возвращают программы по завершению выполнения.
Это то число, которое возвращает функция `main` в программах на Си/C++.
По соглашению число 0 считается признаком успешного завершения программы.

Сценарий также может указать код завершения через команду `exit`, которой передают целое число.
`exit` без аргумента выдает нулевой код завершения.

Код завершения программы сохраняется в переменной `$?`, которую можно вывести на экран командой `echo $?`.

Существуют команды `true` и `false`, которые возвращают нулевой и ненулевой код.
Они могут быть полезны при отладке сценариев.

Другая, наиболее часто используемая команда при написании сценариев, -- это `test`.
Она проверяет файлы и сравнивает строковые и целочисленные значения.
Например, команда `test -e file.sh` проверяет файл на существование.
Если файл существует, то код завершения будет нулевой:
``` console
tim@pc:~$ test -e example.sh1; echo $?
1
tim@pc:~$ test -e example.sh; echo $?
0
```

О других возможностях `test` вы можете узнать в `test(1)`.


## Условные операторы

Условные операторы имеют следующий синтаксис:

```bash
if command1; then
    command2
fi
```

```bash
if command1; then
    command2
else
    command3
fi
```

Здесь, в зависимости от кода возврата команды `command1` выполняется либо ветка c `then`, либо, если существует, ветка `else`.
Но чаще используют другой синтаксис.
Команду `command1` выносят в отдельную строку, а уже внутри `if` используют команду `test`, а если быть точнее, то его переименованную версию `[`.
Таким образом синтаксис условного выражения выглядит как

```bash
if [ condition ]; then
    command2
fi
```

```bash
if [ condition ]; then
    command2
else
    command3
fi
```

```{note}
Команда с коротким названием `[` есть версия `test` с тем же поведением.
Это подверждает команда `man [`.
В отличие от `test` команда `[` требует аргумента `]` в конце.
```

Напишем простой сценарий, который в зависимости от кода возврата вызванной программы выводит на экран сообщение.
```bash
#!/bin/bash
#echo "hello" # <- возвращает нулевой код завершения.
find . --nonexist-option # <- возвращает ненулевой код завершения.
if [ $? -ne 0 ]; then
    echo "Утилита вернула ненулевой код возврата!"
else
    echo "Утилита вернула нулевой код возврата!"
fi
```

Следующие команды проверяют существование файла и/или каталога:
* `[ -e example.sh ]` проверяет существование файла или каталога;
* `[ -f example.sh ]` проверяет существование обычного файла;
* `[ -d .git ]` проверяет существование каталога;

Напишем код сценария, который выполняет последующие действия только из корневого каталога проекта.
Как узнать, что текущий каталог указывает именно на корневой каталог проекта?
Любой проект разработки использует систему контроля версий Git, которая в свою очередь хранит свою базу данных в подкаталоге `.git` в корне проекта.
```bash
#!/bin/bash
if [ ! -d .git/ ]; then
    echo "Сценарий следует запускать только из корневого каталога проекта."
    exit 1
if
```


## Вопросы для самоконтроля

1. Что такое "сценарий"?
1. Как устанавливается код завершения программы на Си/C++ и сценария?
1. Какое расширение имеют файлы со сценариями?
1. Как оформляются комментарии в сценариях `bash`?
1. Как можно объединить несколько команд в одну командную строку?
1. Как можно установить атрибут "исполняемый" у файла?
1. Как можно убрать атрибут "исполняемый" у файла?
1. Что делает строка с шебангом в сценарии?
1. Как запустить файл со сценарием?
1. Какие причины могут помешать запустить файл со сценарием?
1. Что означает "код завершения"?
1. Какие значения может принимать код завершения?
1. Что делают команды `true` и `false`?
   Где они могут быть полезны?
1. Перечислите возможности команды `test`.
1. Какое другое именование имеет команда `test`?
1. Какой синтаксис имеет условный оператор?
   Продемонстрируйте на примере.


## Упражнения

1. Найдите в системе файлы со сценариями.
1. В справочных страницах информация о кодах возврата команд прописывается в разделе "EXIT STATUS".
   Найдите страницы с этим разделом и удостоверьтесь, что 0 -- это успешное завершение утилиты.
1. Напишите программу, которая сравнивает два текстовых файла и печатает сообщение о том, что файлы совпадают или нет.
   Для сравнения используйте утилиту `diff`.
1. Сделайте так, чтобы команда `echo $?` распечатала ненулевое значение.
   Запустите эту же команду `echo $?` еще раз.
   Почему она вернула нулевое значение?
1. Запустите сценарий `bash`, выводящий на экран строку `Hello, Terminal!`, оформленный в кодировке `utf-8` с маркером `BOM`.
   Редактор `vim` устанавливает такую кодировку командой `:set fileencoding=utf-8 bomb`.
   Проверьте присутствие байтов командой `hexdump file.sh`.
   Запустите сценарий и объясните результат.
1. Напишите простой сценарий для интерпретатора `python3`.
   Чтобы найти путь к исполняемому файлу интерпретатора `python3` воспользуйтесь выводом команды `whereis python3`.
1. Создайте в домашнем каталоге подкаталог `bin`, внесите путь к нему в список переменной `PATH` и попробуйте запустить свой сценарий по имени файла.
1. Почему в однострочной команде `if true; then echo success; fi` после `echo success` ставят `;`?
   Если `fi` переместить в новую строку, то `;` уже будет не обязательна для корректного выполнения команды.
1. Код завершения может принимать значения от 0 до 255 (один байт).
   Но функция `main` возвращает число типа `int`, т.е. четыре байта.
   Что будет, если вернуть большее чем 255 число?
1. Продемонстрируйте некоторые возможности команды `test`.
1. Напишите сценарий, который выводит на экран переданные ему аргументы в формате `argument 1 = argument`.
   Используйте условный оператор и следующие предопределенные переменные: `$#` -- количество переданных сценарию аргументов, `$0` -- нулевой аргумент, `$1` -- первый аргумент и так далее. 
1. Напишите сценарий, который возвращает код 0 на аргумент `success`, 1 на аргумент `fail` и 2 на остальные случаи.

<!--
Проверка, что сценарий запущен под правами суперпользователя: `if [ $EUID -eq 0 ];`
Список команд:
* `cmd1; cmd2; cmd3`;
* `cmd1 && cmd2 && cmd3`;
* `cmd1 || cmd2 || cmd3`;
-->
