Лабораторная работа "Сценарии Bash" - efanov/mephi GitHub Wiki
В предыдущей работе вы познакомились с возможностями командной строки и основными стандартными командами, которых достаточно для повседневного использования в задачах администрирования.
По мере усложнения этих задач, возникает необходимость постоянно вводить множество однотипных команд, возможно, с различными параметрами. Для выполнения сложных задач однотипные команды объединяют в специальный файл - сценарий, который потом можно выполнять как обычную команду. Bash является идеальным языком для автоматизации задач администрирования: выполнение команд, работа с файлами и потоками.
Цель данной работы - научиться разрабатывать собственные сценарии на языке Bash.
Знание языка Bash является залогом успешного решения задач администрирования системы, даже если вы не предполагаете заниматься написанием своих сценариев. Многие системные службы и утилиты используют сценарии Bash, поэтому важно понимать, как они работают и уметь исправлять их, когда это необходимо.
Данная работа является подготовительной для выполнения основного задания курса - разработка собственных интерактивных сценариев командной строки для выполнения задач администрирования средств защиты Linux.
Для редактирования сценариев можно использовать один из следующих Open Source редакторов с подсветкой синтаксиса:
- Vim
- Gedit
- Mousepad
- Geany
- MCEdit
- Visual Studio Code
При выполнении данной работы рекомендуется использовать редактор vim, которым можно пользоваться даже в случаях, когда работа в графической среде невозможна - например, при входе по ssh.
- https://learnxinyminutes.com/docs/ru-ru/bash-ru/
- http://www.opennet.ru/docs/RUS/bash_scripting_guide/
- http://www.commandlinefu.com/commands/browse
- https://gist.github.com/LeCoupa/122b12050f5fb267e75f
- http://rus-linux.net/MyLDP/BOOKS/Vim/prosto-o-vim.pdf
В предыдущей работе для того чтобы вывести текст на экран, использовалась команда echo. В данном примере мы напишем для вывода фразы "Hello world!" специальный сценарий.
- Создайте файл hello.sh.
- Откройте данный файл для редактирования в текстовом редакторе.
- Запишите первую строку файла:
#!/bin/bash
. Это так называемый "шебанг" - специальная инструкция, сообщающая операционной системе, что данный файл нужно воспринимать именно как Bash-сценарий и использовать для его выполнения командую оболочку Bash. - Запишите вторую строку в файл:
echo "Hello world!"
. Данная команда выводит на экран фразу "Hello world!". - Выполните полученный сценарий:
bash hello.sh
- Для того чтобы сценарий можно было выполнить как обычную программу, нужно сделать файл сценария исполняемым:
chmod +x hello.sh
- Попробуйте запустить сценарий как обычную программу:
./hello.sh
. - Попробуйте запустить сценарий без указания пути:
hello.sh
. Данная команда не работает, так как она отсутствует в перечне путей в переменной$PATH
.$PATH
содержит список каталогов, разделённых:
, в которых командная оболочка последовательно ищет исполняемый файл. - Создайте в домашнем каталоге каталог
bin
и переместите туда файлhello.sh
. - Попробуйте запустить сценарий без указания пути:
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
, в котором напишите:
- ФИО студента
- Номер группы
И сделайте первый коммит с данным файлом. Не забудьте предварительно настроить git.
$ git config --global user.name "ФИО"
$ git config --global user.email "Адрес электронной почты"
$ git add README
$ git commit -m 'Added README' README
Дальнейшие правила выполнения задания:
-
Все сценарии должны содержаться в каталоге
scripts
. - Каждое задание - отдельный файл со сценарием. Название сценария придумать самостоятельно.
- После написания и тестирования каждого сценария его нужно добавить в git аналогично файлу README и выполнить
git commit
с комментарием: "<порядковый номер задания> - <краткое описание сценария>". - Исправление ошибок должно также сопровождаться коммитом с описанием изменений.
- Задания (примеры) без номера заводить в git не нужно.
Пример. Завершён сценарий № 1.1 раздела "1. Объединение команд", его название - make_shared.sh
.
$ git add make_shared.sh
$ git commit -m '1.1 - создание общего каталога' make_shared.sh
В данном разделе рассмотрим простейшие примеры использования сценариев 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", которая:
- Пожелает пользователю доброго утра.
- Выведет текущее время и календарь на текущий месяц.
- Выведет список дел из файла 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. Разработать сценарий, который выводит файлы из текущего каталога в следующем порядке:
- Каталоги.
- Обычные файлы.
- Символьные ссылки.
- Символьные устройства.
- Блочные устройства.
Формат вывода:
Каталоги:
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
Полезная шпаргалка: 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
В данном разделе необходимо разработать сценарии, которые не только автоматизируют какие-то действия, но и принимают некоторые параметры от пользователя. Таким образом, созданные сценарии можно повторно использовать для различных входных данных.
Аргументы командной строки в сценарий 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 - $(())
.