Написание скриптов на языке Bash #
Андрей Михайлович Васильев, 2023
Версии презентации
Исполняемые файлы в Linux #
Всего можно выделить 2 типа исполняемых файла:
- Бинарные приложения, предназначенные для работы на текущем процессоре
- Интерпретируемые приложения, включая скриптовые языки и интерпетируемые языки
Для запуска первых приложений достаточно только ОС, для работы вторых нужен установленный интерпретатор или соответствующая исполнительная машина
Типичный способ запуска таких приложений выглядит следующим образом:
# интерпретатор файл-приложения
python app.py
ruby app.rb
bash script.shАвтоматический выбор интерпретатора #
Приложение, реализованное на языке Python, обычно знает какой интерпретатор ему нужен, а вот его пользователь может не знать
В UNIX-мире есть механизм, позволяющий указать интерпретатор для запуска файла
- Текстовому файлу должны быть выданы права на выполнение
- В начале текстового файла может быть указана информация, по которой загрузчик программ, автоматически выберет интерпретатор
Shebang #
Строка для выбора интерпретатора называется Shebang:
#!интерпретатор [аргументы]В случае, если в начале файла указана данная строка, то при его запуске загрузчик постарается найти интерпретатор и запустит интерпретатор с указанным файлом
Варианты Shebang #
- #!/bin/sh— запуск интерпретатора Bourne Shell или совместимого с ним.
- #!/bin/bash— запуск файла с помощью интерпретатора Bash.
- #!/usr/bin/python3— запуск файла с помощью системного интерпретатора Python 3
- #!/usr/bin/env ruby— запуск файла с помощью интерпретатора Ruby, поиск которого надо выполнять в PATH пользователя, вызывающего приложение
- #!/bin/false— запретить выполнение файла через запуск, нужно например для файлов, предназначеных только для подключения к другим исполняемым скриптам
Путь к интерпретатору должен быть абсолютным, т.к. данный механизм по умолчанию не использует подсистему поиска по PATH. Если необходима функциональность по поиску интерпретатора, тогда следует использовать /usr/bin/env
Пример добавления информации к скрипту #
Рассмотрим простейшее приложение на языке Bash:
echo 'Hello, world!'Запишем его в файл /home/user/script.sh и дадим права на исполнение
$ cd /home/user
$ chmod +x script.sh
$ ./script.shДля запуска скрипта необходимо:
- либо указать полный путь к исполнямому файлу в качестве команды
- либо поместить файл в один из каталогов PATH
Выбор языка для написания скриптов #
Исполняемые скрипты для выполнения административных задач в Linux можно писать на множестве языков: Python, Ruby, Perl, Bash и так далее
При выборе языка программирования необходимо учитывать множество факторов:
- Сколько времени нужно будет поддерживать скрипт?
- Кто будет поддерживать скрипт?
- Насколько сложную задачу необходимо решать?
- Сложность задачи состоит в последовательности вызова программ или в обработке данных?
- Скрипт должен поддерживать работу на одной или нескольких платформах?
Использование Ruby или Python #
Если задача требует сложных вычислений и её необходимо поддерживать достаточно долго, то рекомендуется использовать более продвинутые языки программирования
Использование Ruby #
- Для настройки систем есть специализированные проекты Chef и Puppet
- Для выполнения действий над файлами можно использовать библиотеку FileUtils
- Для запуска внешних процессов есть мощная библиотека open3
- Для выполнения действий по сети есть библиотеки net-ssh и net-scp
Использование Python #
- Для настройки систем есть специализированные проекты Ansible и Salt
- Для выполнения действий над файлами можно использовать библиотеку shutil
- Для запуска внешних процессов есть библиотека Subprocess
- Для выполнения действий по SSH есть библиотека paramiko
Запуск внешних программ в Bash #
С базовым синтаксисом Bash мы уже знакомы
#!/bin/bash
touch file1
mkdir trash
mv file1 trash
rm -rf trash
mkdir trash
echo "Файл удален!"Bash выполняет каждую строку как отдельную команду, которую вводили в терминале
Документация #
Наиболее полная книжка по Bash называется Advanced Bash Scripting Guide
Известные особенности обработки строк в Bash #
- Расширение путей: *,?,[[:upper:]]
- Расширение скобок: {abc, def}
- Арифметическое расширение: $((5 + 10))
- Расширение переменных: ${VAR}
- Замена команд: $(ls)
Все эти элементы можно и нужно использовать при написании скриптов
Описание переменных #
Для объявления переменной используется синтаксис:
name=[value]Для получения доступа к значению переменной используйте $name
- Пробелы вокруг знака равенства недопустимы
- Если не указать значение, то переменная будет содержать нулевую строку
- Декларирование переменных может происходить в рамках команд alias,declare,typeset,export,readonly,local
- Для отмены значений переменной необходимо использовать unset name
- В названии переменной можно использовать латинские символы, подчёркивания
- В качестве значения может выступать любое строковое выражение, к которому будут применены все расширения
Описание переменных. Пример #
#!/bin/bash
info='Some data goes here'
echo "Значение info: $info" # Просмотр переменной
files=$(ls)  # Сохранение результата вызова ls
echo "$files"$ ls
abc  test.sh
$ ./test.sh
Some data goes here
abc
test.shСо знака # начинаются комментарии
Циклы #
Для обхода набора значений удобно воспользоваться циклами
for index in 1 2 3 4 5 6 7 8 9 10; do
    echo "Индекс: $index"
donefor ((index=0; index < 10; index++)) do
    echo "Число: $index"
doneОбход вывода приложения #
Циклы можно использовать для обработки вывода из приложения
echo 'Файлы, начинающиеся с A'
for file in A*; do
    echo "$file"
doneecho 'Проход по PID процессов'
for pid in $(ps -eo pid); do
    echo "${pid}"
done
  Условный оператор if
  #
Благодаря наличию данного оператора можно говорить, что Bash является языком программирования. Важно понимать, что этот язык — узкоспециализированный, ориентированный на обработку строк и запуск внешних задач, поэтому разрабатывать на нём сложные приложения не стоит
Синтаксис:
if list; then list;
[ elif list; then list; ] ...
[ else list; ]
fiЕсли последняя команда из списка в условии вернёт 0, тогда будут выполнены команды в then-списке, в противном случае будет протестированы команды из elif
Возвращаемое значение #
Стандартное определение функции main на языке Си:
int main(int argc, char* argv[], char** envp) {...}- int argc— количество аргументов
- char* argv[]— массив с аргументами
- char** envp— массив с переменными окружения в формате- КЛЮЧ=ЗНАЧЕНИЕ
- int main, метод возвращает целое значение, обозначающее результат выполнения- 0 — программа выполнила все действия корректно
- !=0 — во время работы программы возникли ошибки, некорректное завершение
 
Проверка возвращаемого значения в Bash #
В BASH переменная ? позволяет узнать статус выполнения последней команды
$ ls
$ echo $?
0
$ ls /aoeb &> /dev/null
$ echo $?
2
  Приложение test
  #
Приложение позволяет проверить некоторые выражения и вернуть 1 или 0
test ВЫРАЖЕНИЕ
[ ВЫРАЖЕНИЕ ]Написанное выражение подвергается обработке со стороны BASH как обычная строка, а после передаётся для выполнения в приложение test
Данное приложение предназначено для использования в if-выражении:
# Testing that file exists
if [ -f /tmp/data ]; then
    echo "Файл /tmp/data существует!"
fi
  Встроенная команда Bash [[
  #
Данная команда является расширением приложения test и исправляет ряд его ограничений. Рекомендуется к использованию вместо приложения test
- Внутри команды [[ ]]не происходит расширения строк
- Можно использовать &&,||внутри выражения
- Легче группировать выражения с помощью (...)
Команду [[ следует использовать для сравнения строк и проверки файлов
  Комбинирование проверок в test
  #
Несколько проверок можно объединить с помощью &&, || на уровне Bash:
[ -f /tmp/data ] || { [ -f /tmp/info ] && [ -f /tmp/more ] }Операторы && и || работают на уровне результатов работы приложения
  Строковые проверки команд test и [[
  #
Строковые проверки отличаются
| [[ | test | Пример | 
|---|---|---|
| > | \> | [[ a > b ]]ложь, a идёт раньше b | 
| < | \< | [[ az < za ]]правда, a идёт раньше z | 
| =,== | = | [[ a = a ]]правда, a равно a | 
| != | != | [[ a != b ]]правда, a не равно b | 
| =,== | нет | [[ name = n* ]]правда, name начинается с n | 
| =~ | нет | [[ home =~ ^h+ ]]правда, home соответствует выражению | 
| -z | -z | [[ -z $info ]]правда, если строка в $info нулевой длины | 
| -n | -n | [[ -n $data ]]правда, если строка в $data ненулевой длины | 
  Числовые проверки команд test и [[
  #
Числовые проверки между test и [[ не отличаются
| Проверка | Пример | 
|---|---|
| -gt | [[ 5 -gt 10 ]]ложь, 5 меньше 10 | 
| -lt | [[ 8 -lt 9 ]]правда, 8 меньше 9 | 
| -eq | [[ 5 -eq 3 ]]ложь, 5 не равно 3 | 
| -ne | [[ 5 -ne 3 ]]правда, 5 не равно 3 | 
| -ge | [[ 3 -ge 3 ]]правда, 3 больше или равно 3 | 
| -le | [[ 3 -le 8]]правда, 3 меньше или равно 8 | 
  Файловые проверки команд test и [[
  #
| Проверка | Пример | 
|---|---|
| -a | [[ -a /tmp/data ]]правда, если файл существует | 
| -d | [[ -d /var/log ]]правда, если директория существует | 
| -e | [[ -e /run/info.pid ]]правда, если файл существует | 
| -f | [[ -f /tmp/test ]]правда, если существует и является файлом | 
| -r | [[ -r ~/info ]]правда, если файл доступен на чтение | 
| -w | [[ -w ~/result ]]правда, если файл доступен на запись | 
| -x | [[ -x ~/bin/run ]]правда, если файл можно выполнять | 
  Циклы while и until
  #
Помимо итеративного for в Bash также есть бесконечные циклы while и utlit:
while list-1; do list-2; done
until list-1; do list-2; done- Цикл whileбудет выполнять свои действия пока условия вlist-1верны
- Цикл untilбудет выполнять свои действия пока условия вlist-1неверны
Выйти из цикла можно с помощью break [n] Если передать число, тогда выход будет произведён из такого количества циклов
Для перехода к следующей итерации можно воспользоваться continue [n] Если передать число, то продолжение будет на соответствующем уровне вложенности
  Пример цикла while
  #
#!/bin/bash
echo
while [ "$var1" != "end" ]
do
  echo "Input variable #1 (end to exit) "
  read var1                    # Not 'read $var1' (why?).
  echo "variable #1 = $var1"   # Need quotes because of "#"
  echo
done
exit 0Типы переменных в Bash #
Bash поддерживает следующие типы переменных:
- Локальные переменные, которые видны внутри области видимости (файла, функции и т.д.)
- Переменные окружения, которые видны детским процессам
- Позиционные переменные
Переменные окружения #
- Переменные окружения доступны не только интерпретатору Bash, но также и процессам, которые он запускает
- Переменные окружения хранит операционная система, при запуске нового процесса копирует переменные окружения запускающего процесса в переменные запускаемого процесса
- Изменение переменной окружения внутри внешнего процесса не приводит к изменению значения внутри интерпретатора
- Переменные окружения являются альтернативным способом к передаче аргументов запускаемому приложению
Декларирование #
Для декларирования переменной окружения используется команда export:
export [name=[word]] ...Отображение переменных окружения #
Для отображения списка переменных окружения доступны приложения env и printenv
- Интерпретатор запускает их как и любые другие приложения
- Приложения выводят список своих переменных окружения, который является копией переменных интерпретатора в момент запуска
Получение значения переменных окружения #
В рамках языка Си доступны следующие способы получения переменных окружения:
- Через аргументы функции main():
int main( int argc, char *argv[], char *envp[] )
- С помощью функций getenvиsecure_getenv#include <stdlib.h> char *getenv(const char *name); char *secure_getenv(const char *name);
Позиционные переменные #
В переменные $0, $1, $2 записываются аргументы скрипта
- $0содержит название скрипта
- $1содержит первый аргумент и т.д.
- После 9 к аргументам необходимо обращаться ${10}
- $*и- $@позволяют обратиться сразу ко всем переменным
- $#позволяет получить количество переданных аргументов
Базовое использование позиционных переменных #
MINPARAMS=10
if [ $# -lt "$MINPARAMS" ]; then
  echo
  echo "This script needs at least $MINPARAMS arguments!"
fi
if [ -n "$1" ]              # Tested variable is quoted.
then
 echo "Parameter #1 is $1"  # Need quotes to escape #
fi
if [ -n "${10}" ]  # Parameters > $9 must be enclosed
then
 echo "Parameter #10 is ${10}"
fiСпециальная обработка аргументов #
Команда shift производит “сдвиг” позиционных аргументов влево
$1 <--- $2, $2 <--- $3, $3 <--- $4, ...Значение переменной $0 не изменяется
С помощью этой команды можно обойти все аргументы:
until [ -z "$1" ]  # Until all parameters used up . . .
do
  echo -n "$1 "
  shift
doneshift может сдвигать сразу на несколько позиций
Проверка качества скриптов #
Язык Bash имеет множество отличий в семантике работы от привычных языков программирования, поэтому надо быть осторожным с переносом своих привычек на язык Bash
Для проверки Bash-кода на наличие проблем рекомендуется использовать статический анализатор кода ShellCheck
Для установки его в GNU/Debian необходимо от имени сперпользователя
apt install shellcheckПример использования ShellCheck #
#!/bin/bash
until [ -z $1 ]  # Until all parameters used up . . .
do
  echo -n "$1 "
  shift
done$ shellcheck test.sh
In test.sh line 3:
until [ -z $1 ]  # Until all parameters used up . . .
           ^-- SC2086: Double quote to prevent globbing and word splitting.
Did you mean:
until [ -z "$1" ]  # Until all parameters used up . . .
For more information:
  https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...