Лабораторная работа "Сценарии Bash" - efanov/mephi GitHub Wiki

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

По мере усложнения этих задач, возникает необходимость постоянно вводить множество однотипных команд, возможно, с различными параметрами. Для выполнения сложных задач однотипные команды объединяют в специальный файл - сценарий, который потом можно выполнять как обычную команду. Bash является идеальным языком для автоматизации задач администрирования: выполнение команд, работа с файлами и потоками.

Цель данной работы - научиться разрабатывать собственные сценарии на языке Bash.

Знание языка Bash является залогом успешного решения задач администрирования системы, даже если вы не предполагаете заниматься написанием своих сценариев. Многие системные службы и утилиты используют сценарии Bash, поэтому важно понимать, как они работают и уметь исправлять их, когда это необходимо.

Данная работа является подготовительной для выполнения основного задания курса - разработка собственных интерактивных сценариев командной строки для выполнения задач администрирования средств защиты Linux.

Для редактирования сценариев можно использовать один из следующих Open Source редакторов с подсветкой синтаксиса:

  • Vim
  • Gedit
  • Mousepad
  • Geany
  • MCEdit
  • Visual Studio Code

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

Материалы для подготовки

Ваш первый сценарий

В предыдущей работе для того чтобы вывести текст на экран, использовалась команда echo. В данном примере мы напишем для вывода фразы "Hello world!" специальный сценарий.

  1. Создайте файл hello.sh.
  2. Откройте данный файл для редактирования в текстовом редакторе.
  3. Запишите первую строку файла: #!/bin/bash. Это так называемый "шебанг" - специальная инструкция, сообщающая операционной системе, что данный файл нужно воспринимать именно как Bash-сценарий и использовать для его выполнения командую оболочку Bash.
  4. Запишите вторую строку в файл: echo "Hello world!". Данная команда выводит на экран фразу "Hello world!".
  5. Выполните полученный сценарий: bash hello.sh
  6. Для того чтобы сценарий можно было выполнить как обычную программу, нужно сделать файл сценария исполняемым: chmod +x hello.sh
  7. Попробуйте запустить сценарий как обычную программу: ./hello.sh.
  8. Попробуйте запустить сценарий без указания пути: hello.sh. Данная команда не работает, так как она отсутствует в перечне путей в переменной $PATH. $PATH содержит список каталогов, разделённых :, в которых командная оболочка последовательно ищет исполняемый файл.
  9. Создайте в домашнем каталоге каталог bin и переместите туда файл hello.sh.
  10. Попробуйте запустить сценарий без указания пути: hello.sh. Так как сценарий теперь расположен по пути, который есть в $PATH, команда должна сработать и вывести на экран "Hello world!".

Примечание. Данный сценарий будет доступен только текущему пользователю. Для того чтобы сценарий работал у всех пользователей системы, его нужно скопировать в каталог /usr/local/bin (для этого потребуются права root).

Порядок выполнения работы

Начиная со следующего раздела вам будет необходимо самостоятельно разработать несколько сценариев на языке Bash. Создайте рабочий каталог, в котором будут содержаться ваши сценарии:

$ mkdir ~/scripts
$ cd ~/scripts

Установите программу git:

$ su
<ввести пароль root>
# yum install -y git
# exit

Инициализируйте в данном каталоге git-репозиторий:

$ git init

Создайте файл README, в котором напишите:

  1. ФИО студента
  2. Номер группы

И сделайте первый коммит с данным файлом. Не забудьте предварительно настроить git.

$ git config --global user.name "ФИО"
$ git config --global user.email "Адрес электронной почты"
$ git add README
$ git commit -m 'Added README' README

Дальнейшие правила выполнения задания:

  1. Все сценарии должны содержаться в каталоге scripts.
  2. Каждое задание - отдельный файл со сценарием. Название сценария придумать самостоятельно.
  3. После написания и тестирования каждого сценария его нужно добавить в git аналогично файлу README и выполнить git commit с комментарием: "<порядковый номер задания> - <краткое описание сценария>".
  4. Исправление ошибок должно также сопровождаться коммитом с описанием изменений.
  5. Задания (примеры) без номера заводить в git не нужно.

Пример. Завершён сценарий № 1.1 раздела "1. Объединение команд", его название - make_shared.sh.

$ git add make_shared.sh
$ git commit -m '1.1 - создание общего каталога' make_shared.sh

1. Автоматизация выполнения команд

В данном разделе рассмотрим простейшие примеры использования сценариев Bash - объединение нескольких однотипных команд. Это простейшие сценарии, которые не принимают аргументов, не содержат сложных условий, циклов и т.п.

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

  • создать каталог;
  • изменить права доступа для каталога.

1.1. Разработать сценарий, который создаёт каталог /tmp/shared и устанавливает на него права доступа rwxrwxrwx.

Пример решения данной задачи на языке сценариев Bash:

#!/bin/bash
DIR=/tmp/shared
mkdir -p "$DIR"
chmod 777 "$DIR"

1.2. Вывести количество файлов в домашнем каталоге, которые заканчиваются на .txt. Создайте несколько таких файлов для тестирования.

1.3. Вывести текущие переменные окружения в отсортированном по алфавиту порядке.

1.4. Разработать программу "Good morning", которая:

  1. Пожелает пользователю доброго утра.
  2. Выведет текущее время и календарь на текущий месяц.
  3. Выведет список дел из файла TODO домашнего каталога пользователя.

1.5. Найти и вывести пути до файлов из каталога /usr (включая подкаталоги), размер которых больше 20 Мб. Подсказка: man find.

1.6. Подсчитать количество файлов, количество скрытых файлов в домашнем каталоге текущего пользователя и вывести результат в формате:

Домашний каталог пользователя
<User>
содержит обычных файлов:
XX
скрытых файлов:
YY

1.7. Вывести на экран дату, время, список активных пользователей в системе на данный момент, время работы системы с момента последней перезагрузки.

1.8. Вывести количество процессов, запущенных от имени текущего и от имени пользователя root в формате:

Процессов пользователя:
<User>
XX
Процессов пользователя root:
YY

1.9. Найти и вывести 5 процессов, потребляющих больше всего памяти в системе. Подсказка: man ps

1.10. Вывести файлы и каталоги из домашнего каталога пользователя, упорядочив их по размеру. Подсказка: использовать команды du и sort.

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

  1. Каталоги.
  2. Обычные файлы.
  3. Символьные ссылки.
  4. Символьные устройства.
  5. Блочные устройства.

Формат вывода:

Каталоги:
drwxr-xr-x  2 root root           560 сен 13 01:34 block/
drwxr-xr-x  2 root root           120 сен 13 01:34 bsg/
drwxr-xr-x  3 root root            60 июн 19 06:41 bus/
drwxr-xr-x  2 root root          3680 сен 13 01:34 char
...
Обычные файлы:
...
Символьные ссылки:
...
Символьные устройства:
...
Блочные устройства:
...

Когда сценарий будет готов, скопируйте его в каталог ~/bin для тестирования. Далее протестируйте сценарий для каталогов /, /dev, /tmp.

Подсказка: ls -l /dev | grep ^b

2. Перенаправление стандартного ввода/вывода

Полезная шпаргалка: http://www.catonmat.net/download/bash-redirections-cheat-sheet.pdf

Краткий справочник:

  • > - записать стандартного вывод (stdout) в файл (содержимое файла будет безвозвратно утеряно)
  • 1> - полностью аналогично предыдущему
  • 2> - аналогично, но для вывода ошибок (stderr)
  • &> - аналогично, но для двух стандартных выводов сразу - stderr и stdout
  • >> - дописать стандартный вывод (stdout) в файл (содержимое файла будет сохранено)
  • 2>> - аналогично, но для stderr
  • &>> - аналогично, но для двух стандартных выводов сразу - stderr и stdout
  • < file - перенаправить файл в стандартный ввод (stdin)
  • > &2 - всё, что программа выводит в stdout, будет перенаправлено в stderr
  • ls | wc - перенаправляет stdout ls в stdin wc
  • ls |& wc - перенаправляет stderr и stdout ls в stdin wc
  • /dev/null - универсальный файл-"чёрная дыра", в неё можно отправить любой ненужный вывод
  • : - универсальная команда, которая не выводит ничего и всегда возвращает истинный код возврата, в неё можно отправить любой ненужный вывод

Обычно перенаправление вывода размещают после команды - наиболее логичным способом:

echo error >&2
ls > /tmp/list

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

>&2 echo error
> /tmp/list ls -l
< /tmp/list grep 1

Основная сложность проявляется в том, что и stderr и stdout по умолчанию выводятся на экран и никак не отличаются друг от друга. Проверить stderr это или stdout можно перенаправлением в /dev/null.

Например:

ls /non-existent
ls: невозможно получить доступ к /non-existent: Нет такого файла или каталога
# попробуем спрятать ошибку. Так не работает, так как ошибка выводится в stderr
ls /non-existent > /dev/null
# теперь пробуем перенаправить поток ошибок:
ls /non-existent 2> /dev/null
# получилось! Теперь на экран ничего не выводится

# можно заблокировать любой вывод программы
# это нужно, когда мы просто хотим узнать код возврата
ls /non-existent &> /dev/null
# данный способ годится только для Bash
# в более старых системах приходилось делать так:
ls /non-existent 2> /dev/null >&2

Частая ошибка - попытка найти что-то в выводе программы с помощью grep, но вывод осуществляется "мимо" stdout, в итоге мы получим весь вывод просто на экран.

# вы можете использовать grep, чтобы найти нужный параметр в справке
ls --help | grep ссыл
                             следовать по символьным ссылкам в командной
                             следовать по всем символьным ссылкам в командной
  -L, --dereference          показывая информацию для символьной ссылки,
                             показывать информацию о файле, на который ссылка
                             ссылается
# но с ошибками не так просто! Эта команда никак не поможет с поиском:
ls /{1..100} | grep 99
# чтобы найти ошибку в гуще других, нужно чуть изменить команду
ls /{1..100} |& grep 99

Удобство перенаправления потоков в Bash (по сравнению с такими языками как C) компенсируется тем, что символы >, <, |, которые нередко встречаются в повседневной работе, нужно экранировать.

Некоторые команды меняют свой вывод в зависимости от того, перенаправлен ли их вывод или нет:

ls /sbin
NetworkManager         fsck.ext4                    lvmsadc                                  setenforce
accessdb               fsck.minix                   lvmsar                                   setfiles
addgnupghome           fsck.xfs                     lvreduce                                 setsebool
addpart                fsfreeze                     lvremove                                 sfdisk
...

ls /sbin | head
NetworkManager
accessdb
addgnupghome
addpart
adduser
agetty
alternatives
anacron
applygnupgdefaults
arpd

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

Вы можете создавать сценарии, которые также принимают на стандартный ввод (stdin) некий ввод и обрабатывает его с помощью стандартных команд. Для этого нужно просто не указывать файл для обрабатывающей команды. Например, нужно разработать сценарий (mygrep.sh), который принимает на стандартный ввод текст и выводит строки, совпадающие с заранее запрограммированным шаблоном.

#!/bin/bash
grep -i bash

Данный сценарий будет фактически упрощённым аналогом команды grep, где все необходимые аргументы уже заданы. Его можно использовать следующим образом:

cat /etc/passwd | ./mygrep.sh
< /etc/passwd ./mygrep.sh

Для тестирования следующих заданий выполните подготовительные действия:

  • создайте в домашнем каталоге файл numbers.txt, в который запишите 10 000 натуральных чисел (см. команду seq);
  • создайте в домашнем каталоге файл users.txt, в который запишите имена всех пользователей системы (используйте cut и /etc/passwd);
  • создайте в домашнем каталоге файл bash.txt, в который запишите содержимое двоичного файла /bin/bash в текстовом виде (используйте od);
  • создайте в домашнем каталоге файл services.txt, который будет идентичным файлу /etc/services (скопировать файл с новым именем).

2.1. Разработать сценарий, который ведёт в файле /tmp/run.log последовательный журнал запусков:

  • в конец журнала добавляет строку с датой и временем запуска сценария (используйте команду date для фиксации даты и времени запуска сценария);
  • в стандартный вывод (stdout) - выводит фразу "Hello, World!"
  • в стандартный вывод ошибок (stderr) - выводит количество предыдущих запусков программы (для этого достаточно подсчитать количество строк в журнале).

Убедиться в правильности работы программы и выводе различных сообщений в различные потоки вывода:

2.1.sh > /dev/null # должен вывести счётчик запусков, счётчик должен увеличиваться
2.1.sh 2> /dev/null # должен вывести Hello, World!

2.2. Разработать сценарий, который запускает сценарий, разработанный в предыдущем задании, а также открывает журнал запусков предыдущего сценария в программе less.

2.3. Разработать сценарий, который для bash.txt (созданного ранее):

  • сохранит строки, которые содержат сочетание символов 000000 в файл /tmp/zeros;
  • сохранит строки, которые не содержат сочетания символов 000000 - в файл /tmp/nozeros;
  • выведет 10 первых и 10 последних строк от каждого из файлов /tmp/zeros и /tmp/nozeros.

2.4. Разработать сценарий, который считывает построчно стандартный ввод и выводит только те строки, которые содержат слово bin целиком в стандартный вывод ошибок. Для проверки сценария используйте конвейер с командой 'ls /.

2.5. Разработать сценарий, который для всех файлов с расширением txt в домашнем каталоге пользователя:

  • выведет список таких файлов;
  • выведет суммарный размер в байтах и строках для файлов с расширением txt.

Подсказка. Для решении этой задачи создайте временный файл в каталоге /tmp, по окончании работы сценария удалите его.

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

Для подготовки выполните команды:

mkdir dups
cd dups
touch {1..3}.txt
echo 4 > 4.txt
echo 4 > 5.txt
echo 6 > 6.txt

Подсказка. Наиболее эффективным методом поиска дубликатов является вычисление их хэш-суммы с последующей их обработкой. У файлов-дубликатов хэш-суммы будут одинаковыми. Используйте команды sort, uniq, grep, tr, cut. Изучите как команды sort и uniq работают с колонками.

md5sum *.txt | ???? -k? | ???? -? -? ?? | ... | ... | ...

Должен получиться вывод:

2	4.txt
3	1.txt

3. Аргументы командной строки и переменные

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

Аргументы командной строки в сценарий Bash передаются также как и в обычную программу вроде grep или wc. Внутри сценария используются специальные переменные $1, $2 и т.п. - по порядку аргументов.

$ ./myscript.sh foo bar 1 2 3
Первые два аргумента: foo bar
Сумма третьего, четвёртого и пятого аргументов: 6
$ cat myscript.sh
#!/bin/bash
echo "Первые два аргумента: $1 $2"
echo "Сумма третьего, четвёртого и пятого аргументов: $(($3+$4+$5))"

Исследуйте, как ведут себя специальные переменные Bash самостоятельно:

$1, $2 - первый аргумент, второй аргумент и т.п.; $# - количество аргументов командной строки, переданные сценарию; $* - все аргументы, переданные сценарию, объединённые в один; $@ - все аргументы, переданные сценарию, по отдельности.

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

# НЕПРАВИЛЬНО!
echo $1 $2 $3
grep foo $1
echo $*
du $@ 
# Правильно:
echo "$1 $2 $3"
grep foo -- "$1"
echo "$*"
du -- "$@"
# исключение составляет переменная $#, это всегда число:
grep -m$# /etc/passwd -- "$1"
# при передаче аргумента (напр., имени файла) в другую команду, всегда отделяйте
# пользовательские данные от аргументов команды символами --. Это гарантирует,
# что ваша команда будет работать даже для файлов, которые начинаются с символа -.
grep "$1" /etc/passwd # неправильно, будет ошибка, если $1 == --help
grep /etc/passwd -- "$1" # правильно

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

NAME=Вася
# если в данных содержится пробел, их надо экранировать
FIO="Иванов Иван Иванович"
# в переменную удобно записывать вывод другой команды:
USERS=$(grep /bin/bash /etc/passwd | cut -d: -f1)
# при копировании одной переменной в другую экранирование не требуется:
FIO_COPY=$FIO # экранирование не нужно
# аналогично, не нужно экранирование при записи в переменную 
# результата работы какой-либо команды
FILES=$(ls) # экранирование не нужно
# однако при объединении нескольких команд без экранирования не обойтись:
FIO_AND_ID="$FIO $(id)" # требуется экранировать, так как объединяем результат

Некоторые полезные переменные:

$USER # имя текущего пользователя
$HOST # имя узла
$HOME # путь до домашнего каталога текущего пользователя
$RANDOM # случайное число
$LANG # текущие настройки локализации (язык и кодировка)
$$ # PID текущего процесса (сценария)
$? # код возврата предыдущей команды

3.1. Разработать сценарий, который выводит на экран количество переданных ему аргументов. Скопировать его в $HOME/bin для дальнейшего использования его в других сценариях.

3.2. Разработать сценарий, который вызывает предыдущий сценарий дважды: первый раз с объединённым полным списком аргументов, второй раз - со списком всех переданных ему аргументов по отдельности.

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

  • с аргументами "1" "2" "3";
  • с пятью случайными числами (см. переменную $RANDOM);
  • с аргументами "foo" "bar" "foobar" "foo bar";
  • с аргументами "foo" "--foo" "--help" "-l".

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

3.4. Перепишите задание 1.11. так, чтобы исследуемый каталог передавался через аргумент командной строки.

3.5. Разработать сценарий, который вызывает команду grep и принимает следующие аргументы:

  • текст, который нужно найти;
  • файл, в котором нужно найти этот текст;
  • максимальное количество строк, которое нужно вывести на экран.

Вывод команды grep отсортировать и пронумеровать.

3.6. Разработать сценарий, который выводит в одну строку имя пользователя, его домашний каталог, а также количество символов в этих двух переменных. Например: root /root 9. Подсказка: изучите аргументы команды echo, wc, математические вычисления в Bash - $(()).

⚠️ **GitHub.com Fallback** ⚠️