10  (Доп.) Язык исполняемой оболочки bash

Внимание

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

10.1 Дополнительная теория

Во время долгой рутинной работы: неважно, компьютерной или любой другой - возникает непреодолимое желание автоматизировать её. Автоматизировать свою работу за компьютером позволяет широкий функционал языка сценариев исполняемой оболочки. С егопомощью мы можем заменить какую-то сложную последовательность действий одной операцией. bash (Bourne again shell) является одной из исполняемых оболочек, самой известной. Оболочка может быть разной в зависимости от дистрибутива или операционной системы. Помимо самого терминала (shell) и bash, также существуют оболочки ksh (Korn shell), csh (C shell), zsh (Z shell). Все они немного отличаются между собой, но во много очень сильно похожи. Мы рассматриваем bash как наиболее популярную.

В самом простом варианте сценарий (скрипт) исполняемой оболочки является обычным текстовым файлом с расширением .sh, в котором записаны те же самые команды из командной строки, которые вы запускали до это вручную. Создадим с вами простой скрипт.

Задание

Создайте файл first_example.sh с помощью редактора vim. Напишите в него команду, которая выводит на экран текущее время, затем с новой строки команду, выводящую текущую рабочую директорию. Сохраните файл.

date
pwd

Чтобы проверить работу нашего скрипта, нужно выдать файлу права на запуск. Вспомните как это сделать?

chmod u+x first_example.sh
# Строки, начинающиеся с решетки это комменатарии. Они не выполняются и носят пояснительную функцию 
# Более короткая форма записи команды ниже: ./first_example.sh
bash first_example.sh

Если вы увидели на выводе терминала дату и текущую директорию, вы большие молодцы и написали свой первый скрипт на bash! Если что-то пошло не так, вернитесь на шаг назад и подумайте над сообщениями об ошибках. В случае затруднения обратитесь к преподавателю.

Хорошей практикой считается прописывать для каждого скрипта чем его запускать. Для этого первая строка в файле должна начинаться с шебанга1 (shebang) - последовательности из двух символов: решетки и восклицательного знака. После него в этой же строке, без пробела, должен быть прописан путь к интерпретатору.

Вопрос

Как узнать, где лежит интерпретатор?

which bash

Второй момент связан с тем, что bash скрипт не останавливает своё выполнение, если в каком-то месте возникла ошибка. Для избежания этого второй строкой прописывают команду set,которая имеет несколько опций. При таких настройках некоторые распространенные ошибки приведут к немедленному сбою скрипта, явному и громкому. В противном случае вы можете получить скрытые ошибки, которые будут обнаружены только тогда, когда они взорвут ваш продакшн.

  • set -e

    • Эта опция предписывает bash немедленно завершить работу, если любая команда имеет ненулевой статус выхода. Вы бы не хотели устанавливать это для своей командной строки, но в скрипте это очень полезно. Во всех широко используемых языках программирования общего назначения - необработанная ошибка времени выполнения

    • будь то выброшенное исключение в Java, ошибка сегментации в C или синтаксическая ошибка в Python — выполнение программы немедленно останавливается; последующие строки не выполняются. По умолчанию bash этого не делает. Это поведение по умолчанию — именно то, что вам нужно, если вы используете bash в командной строке. Вы не хотите, чтобы опечатка вывела вас из системы! Но в скрипте вы хотите наоборот. Если одна строка в скрипте не срабатывает, но последняя строка срабатывает, весь скрипт имеет успешный код выхода. Это позволяет очень легко пропустить ошибку. Опять же, то, что вы хотите при использовании bash в качестве командной строки и при использовании его в скриптах, здесь не совпадает. Быть нетерпимым к ошибкам гораздо лучше в скриптах, и это то, что вам дает set -e.

  • set -x

    • Включает режим оболочки, в котором все выполненные команды выводятся на терминал. В вашем случае это явно используется для отладки, что является типичным вариантом использования set -x: вывод каждой команды по мере ее выполнения может помочь вам визуализировать поток управления скрипта, если он не функционирует так, как ожидалось.
  • set -u

    • Влияет на переменные. Если установлена ссылка на любую переменную, которую вы ранее не определили — за исключением $*и $@— является ошибкой и приводит к немедленному завершению программы. Такие языки, как Python, C, Java и другие, ведут себя одинаково, по разным причинам. Одна из них — чтобы опечатки не создавали новые переменные без вашего ведома. Например:

      #!/bin/bash 
      firstName="Aaron" 
      fullName="$firstname Maxwell" 
      echo "$fullName" 

      Остановитесь на минутку и посмотрите. Видите ошибку? В правой части третьей строки написано “firstname” строчными буквами, а не “firstName” в верблюжьем регистре. Без опции -u это будет молчаливая ошибка. Но с опцией -u скрипт завершается на этой строке с кодом выхода 1, выводя сообщение “firstname: unbound variable” в stderr.

    • Это то, чего вы хотите: чтобы сбой произошел явно и немедленно, а не создавал скрытые ошибки, которые могут быть обнаружены слишком поздно.

  • set -o pipefail

    • Эта настройка предотвращает маскировку ошибок в конвейере. Если какая-либо команда в конвейере завершается неудачей, этот код возврата будет использоваться как код возврата всего конвейера. По умолчанию код возврата конвейера — это код последней команды, даже если она выполнена успешно. Представьте себе поиск отсортированного списка совпадающих строк в файле:

      set -o pipefail
      grep some-string /non/existent/file | sort
      # grep: /non/existent/file: No such file or directory
      echo $?
      # 0
    • Здесь grep имеет код выхода 2, записывает сообщение об ошибке в stderr и пустую строку в stdout.

    • Затем эта пустая строка передается через сортировку, которая успешно принимает ее как допустимые входные данные и возвращает код состояния 0.

    • Это хорошо для командной строки, но плохо для сценария оболочки: вы почти наверняка захотите, чтобы сценарий сразу же завершился с ненулевым кодом выхода… например так:

      set -o pipefail
      grep some-string /non/existent/file | sort
      # grep: /non/existent/file: No such file or directory
      echo $?
      # 2

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

Распространненные коды выхода
Код Причина
0 Успех
1-255 Провал
1 Общая ошибка
2 Неправильное использование команды
13 Доступ запрещён
126 Команда найдена, но неисполняема
127 Команда не найдена
Вывод

Хорошей практикой считается начинать свой скрипт с этих двух инструкций.

10.1.1 Переменные и типы данных

Чтобы определить переменную, необходимо задать ей имя и присвоить значение. Для того, чтобы обратиться к переменной, необходимо использовать знак доллара.

#| code-fold: show
#| eval: true
#NB! Никаких пробелов!
country="Russia"
echo $country

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

  1. Имена переменных должны начинаться с буквы или символа подчеркивания ( _).

  2. Имена переменных могут содержать буквы, цифры и символы подчеркивания ( _).

  3. Имена переменных чувствительны к регистру.

  4. Имена переменных не должны содержать пробелов и специальных символов.

  5. Используйте описательные имена, отражающие назначение переменной.

  6. Избегайте использования зарезервированных ключевых слов, таких как if, then, else, fiи т. д., в качестве имен переменных.

Допустимые имена

#| code-fold: show
name
count
_var
myVar
MY_VAR

Недопустимые имена

#| code-fold: show
2ndvar
my var
my-var

Bash довольно скуп на типы данных. Для него существуют только числа с фиксированной запятой, строки, массивы и словари. Интерпретатор сам определяет тип переменной, но его можно задать явно.

#| code-fold: show

# Целое число
declare -i myvar
# Массив
declare -a myvar
# Словарь
declare -A myvar

# Также есть дополнительные полезные опции, см man declare

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

#| code-fold: show
#| eval: true
var1=5
var2=10
var3=$var1+$var2
echo $var3

10.1.2 Специальные переменные

В этой таблице перечислены некоторые специальные переменные.

Перечень специальных переменных
Обращение к переменной Функция
$# Сколько аргументов командной строки было передано
$@ Все переданные аргументы командной строки
$? Выходной статус последнего запущенного процесса
$$ Идентификатор процесса запущенного скрипта
$USER Имя пользователя
$HOSTNAME Имя машины
$SECONDS Количество секунд, которое выполняется скрипт
$RANDOM Случайное число
$LINENO Номер тукщей строки в скрипте
$PWD Текущая рабочая директория

Кроме того, можно обратиться непосредственно к конкретному аргументу командной строки по его номеру от $1 до $9. $0 - имя скрипта.

Задание

Модифицируйте пример выше с сложением двух чисел, чтобы он работал с аргументами командной строки.

var1=$1
var2=$2
var3=$var1+$var2
echo $var3

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

10.1.3.1 Оператор if-else

Синтаксис

if [[ condition ]]; then
    statement1
elif [[ condition ]]; then
    statement2 
else
    default statement
fi
Внимание!

bash не использует привычные операторы сравнения для чисел! Вместо них он использует специальные обозначения. Приведенные в таблице ниже

Операторы сравнения чисел в bash
Оператор Значение
-lt Меньше чем
-le Меньше или равно
-gt Больше чем
-ge Больше или равно
-eq Равно
-ne Не равно

Для сравнения строк используются также свои операторы.

Операторы сравнения строк в bash
Выражение Значение
string1 = string2

Оператор равенства возвращает значение true, если операнды равны.

Используйте =оператор с test [командой.

string1 == string2

Оператор равенства возвращает значение true, если операнды равны.

Используйте ==оператор с [[командой для сопоставления с образцом.

string1 != string2 Оператор неравенства возвращает значение true, если операнды не равны.
string1 =~ regex Оператор регулярного выражения возвращает значение true, если левый операнд соответствует расширенному регулярному выражению справа.
string1 > string Оператор «больше» возвращает значение true, если левый операнд больше правого, отсортированного в лексикографическом (алфавитном) порядке.
string1 < string Оператор «меньше» возвращает значение true, если левый операнд больше правого, отсортированного в лексикографическом (алфавитном) порядке.
-z string Строка нулевой длины
-n string Строка ненулевой длины
#| code-fold: show
#| eval: true
programmist="I use R!"
# Типичное поведение неумных фанатов Python.
# Сравниваемые переменныелучшезаключать в кавычки, чтобы были уверены, что сравниваете строки.
if [[ "$programmist" = "I love Python!" ]]; then
  echo "You are good!"
else
  echo "R is shit! Use Python!"
fi
Задание

Напишите скрипт, принимающий на вход число и проверяющий его знак.

number=$1
if [[ $number -gt 0 ]]; then
    echo "Positive"
elif [[ $number -lt 0 ]]; then
    echo "Negative" 
else
    echo "Zero"
fi

10.1.3.2 Оператор case

Синтаксис

case EXPRESSION in

  PATTERN_1)
    STATEMENTS
    ;;

  PATTERN_2)
    STATEMENTS
    ;;

  PATTERN_N)
    STATEMENTS
    ;;

  *)
    STATEMENTS
    ;;
esac

Оператор case необходим для выбора из ограниченного спектра вариантов. Часто используют в bash для парсинга именованных аругментов командной строки совместно с циклом while. Пример будет ниже.

10.1.4 Циклы

10.1.4.1 Цикл с предусловием

Синтаксис

while condition
do
  statement1
  statement2
done

Пример: парсинг аргументов командной строки

while getopts u:a:f: flag
do
    case "${flag}" in
        u) 
          username=${OPTARG}
        ;;
        a) 
          age=${OPTARG}
        ;;
        f) 
          fullname=${OPTARG}
        ;;
    esac
done
echo "Username: $username";
echo "Age: $age";
echo "Full Name: $fullname";

10.1.4.2 Цикл с постусловием

Вопрос

Чем отличаются цикл с предусловием и постусловием?

Синтаксис

until [CONDITION] 
do   
  [COMMANDS] 
done

Следующий скрипт может быть полезен, когда ваш хост git находится в состоянии простоя, и вместо того, чтобы вручную вводить git pullнесколько раз, пока хост не будет в сети, вы можете запустить скрипт один раз. Он будет пытаться вытащить репозиторий, пока это не будет успешно.

until git pull &> /dev/null
do
    echo "Waiting for the git host ..."
    sleep 1
done

echo -e "\nThe git repository is pulled."

10.1.4.3 Параметрический цикл

Цикл for имеет две формы записи. Первая форма используется для перебора массивов, вторая - для повторения операций заданное число раз

for name in words
do 
commands
done
for (( i=0 ; i<=10 ; i++ ))
do 
commands
done

10.1.5 Массивы

Массивы

array=( 1,2,3,4,5)
for i in array[@]
do
  echo $i
done  

10.1.6 Чтение файла

while IFS= read -r line; do
    echo "Text read from file: $line"
done < my_filename.txt

Также полезны sed и awk (особенно для таблиц).

10.2 Задание

10.2.1 Упражнение 1: Простой скрипт для вывода информации

Напишите скрипт, который:

  1. Выводит текущую дату и время.

  2. Выводит имя пользователя.

  3. Выводит текущую директорию.

Пример вывода:

Текущая дата и время: 2023-10-05 14:30:00 
Имя пользователя: user 
Текущая директория: /home/user

10.2.2 Упражнение 2: Скрипт для работы с файлами

Напишите скрипт, который:

  1. Создает директорию с именем test_dir.

  2. Внутри этой директории создает 5 файлов с именами file1.txt, file2.txt, …, file5.txt.

  3. Записывает в каждый файл строку “Это файл номер N”, где N — номер файла.

  4. Выводит содержимое всех файлов в терминал.

10.2.3 Упражнение 3: Скрипт для поиска файлов

Напишите скрипт, который:

  1. Принимает на вход два аргумента: директорию и расширение файла (например, .txt).

  2. Ищет все файлы с указанным расширением в указанной директории и её поддиректориях.

  3. Выводит список найденных файлов с их полными путями.

Пример использования:

./find_files.sh /home/user .txt

10.2.4 Упражнение 4: Скрипт для анализа системы

Напишите скрипт, который:

  1. Выводит информацию о системе: имя хоста, версия ядра, объем оперативной памяти, объем свободного места на диске.

  2. Сохраняет эту информацию в файл system_info.txt.

  3. Если файл уже существует, скрипт должен спросить пользователя, перезаписать ли его.

10.2.5 Упражнение 5: Скрипт для работы с пользователями

Напишите скрипт, который:

  1. Принимает на вход имя пользователя.

  2. Проверяет, существует ли пользователь в системе.

  3. Если пользователь существует, выводит его домашнюю директорию и список процессов, запущенных от его имени.

  4. Если пользователь не существует, предлагает создать его.

10.2.6 Упражнение 6: Скрипт для резервного копирования

Напишите скрипт, который:

  1. Принимает на вход директорию, которую нужно скопировать, и директорию, куда нужно сохранить резервную копию.

  2. Создает архив с именем backup_YYYY-MM-DD.tar.gz, где YYYY-MM-DD — текущая дата.

  3. Проверяет, существует ли уже архив с таким именем, и если да, предлагает пользователю перезаписать его или создать новый с другим именем.

10.2.7 Упражнение 7: Скрипт для мониторинга процессов

Напишите скрипт, который:

  1. Запускается в фоновом режиме.

  2. Каждые 5 секунд проверяет, запущен ли процесс с именем, указанным в аргументе.

  3. Если процесс не запущен, скрипт выводит сообщение и завершает работу.

Пример использования:

./monitor_process.sh firefox

10.2.8 Упражнение 8: Скрипт для работы с текстовыми файлами

Напишите скрипт, который:

  1. Принимает на вход текстовый файл.

  2. Подсчитывает количество строк, слов и символов в файле.

  3. Выводит эту информацию в терминал.

Пример использования:

./text_stats.sh document.txt

10.2.9 Упражнение 9: Скрипт для работы с сетью

Напишите скрипт, который:

  1. Принимает на вход IP-адрес.

  2. Проверяет доступность этого IP-адреса с помощью команды ping.

  3. Если IP-адрес доступен, выводит сообщение “IP доступен”.

  4. Если IP-адрес недоступен, выводит сообщение “IP недоступен”.

Пример использования:

./check_ip.sh 192.168.1.1

10.2.10 Упражнение 10: Циклы и пайплайны

Напишите скрипт, который:

  1. Используя цикл for, выводит числа от 1 до 10.

  2. Затем с помощью пайплайна (|) передаст эти числа команде grep, чтобы отфильтровать только четные числа.

  3. Выведет результат в терминал.

Пример вывода:

2 4 6 8 10

10.2.11 Упражнение 11: Поиск с помощью grep

Напишите скрипт, который:

  1. Принимает на вход директорию и строку для поиска.

  2. Рекурсивно ищет все файлы в указанной директории, содержащие эту строку.

  3. Выводит имена файлов и строки, в которых найдено совпадение.

Пример использования:

./search_text.sh /home/user "hello world"

10.2.12 Упражнение 12: Замена текста с помощью sed

Напишите скрипт, который:

  1. Принимает на вход текстовый файл и две строки: старая_строка и новая_строка.

  2. Использует команду sed для замены всех вхождений старой_строки на новую_строку в файле.

  3. Сохраняет изменения в том же файле.

Пример использования:

./replace_text.sh file.txt "old_text" "new_text"

10.2.13 Упражнение 13: Комбинация grep, sed и awk

Напишите скрипт, который:

  1. Принимает на вход текстовый файл.

  2. Использует grep для поиска строк, содержащих слово “error”.

  3. Использует sed для удаления всех цифр из найденных строк.

  4. Использует awk для вывода только первых 5 символов каждой строки.

Пример использования:

./process_errors.sh logfile.txt

10.2.14 Упражнение 14: Цикл с условием

Напишите скрипт, который:

  1. Используя цикл while, читает строки из файла построчно.

  2. Если строка содержит слово “warning”, выводит её в терминал.

  3. Если строка содержит слово “error”, завершает выполнение скрипта с сообщением “Обнаружена ошибка!”.

Пример использования:

./check_log.sh logfile.txt

10.2.15 Упражнение 15: Пайплайны и awk

Напишите скрипт, который:

  1. Использует команду df для получения информации о дисковом пространстве.

  2. Передает вывод через пайплайн в awk, чтобы отфильтровать только те разделы, где использование превышает некоторый порог.

  3. Выводит список таких разделов.

Пример вывода:

/dev/sda1 85% /dev/sdb2 90%

10.2.16 Упражнение 16: Анализ CSV с помощью awk

Напишите скрипт, который:

  1. Принимает на вход CSV-файл с данными (например, data.csv).

  2. Использует awk для подсчета суммы значений в определенной колонке (например, второй колонке).

  3. Выводит результат.

Пример CSV:

Name,Amount 
Alice,100 
Bob,200 
Charlie,300

Пример использования:

./sum_column.sh data.csv 2

Пример вывода:

Сумма: 600

10.3 Вопросы для защиты

  1. Выполнение скрпитов в bash

  2. Переменные и типы данных в bash

  3. Условия и циклы в bash

  4. Статусы выхода. Сигналы

  5. Специальные переменные, позиционные аргументы


  1. непереводимый термин↩︎