Написание скриптов на языке Bash #

Андрей Михайлович Васильев, 2022

Версии презентации

Исполняемые файлы в 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"
done
for ((index=0; index < 10; index++)) do
    echo "Число: $index"
done

Обход вывода приложения #

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

echo 'Файлы, начинающиеся с A'
for file in A*; do
    echo "$file"
done
echo 'Проход по 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 поддерживает следующие типы переменных:

  • Локальные переменные, которые видны внутри области видимости (файла, функции и т.д.)
  • Переменные окружения, которые видны детским процессам
  • Позиционные переменные

Переменные окружения #

Для декларирования переменной окружения используется команда export:

export [name=[word]] ...

Позиционные переменные #

В переменные $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
done

shift может сдвигать сразу на несколько позиций

Проверка качества скриптов #

Язык 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 ...