[https://habr.com/ru/company/ruvds/blog/328346/](https://habr.com/ru/company/ruvds/blog/328346/) --- В предыдущих материалах мы обсуждали различные аспекты разработки bash-скриптов, говорили о полезных инструментах, но до сих пор рассматривали лишь небольшие фрагменты кода. Пришло время более масштабных проектов. А именно, здесь вы найдёте два примера. Первый — скрипт для отправки сообщений, второй пример — скрипт, выводящий сведения об использовании дискового пространства. Главная ценность этих примеров для тех, кто изучает bash, заключается в методике разработки. Когда перед программистом встаёт задача по автоматизации чего бы то ни было, его путь редко бывает прямым и быстрым. Задачу надо разбить на части, найти средства решения каждой из подзадач, а потом собрать из частей готовое решение. ## Отправка сообщений в терминал пользователя В наши дни редко кто прибегает к одной из возможностей Linux, которая позволяет общаться, отправляя сообщения в терминалы пользователей, вошедших в систему. Сама по себе команда отправки сообщений, `write`, довольно проста. Для того, чтобы ей воспользоваться, достаточно знать имя пользователя и имя его терминала. Однако, для успешной отправки сообщения, помимо актуальных данных о пользователе и терминале, надо знать, вошёл ли пользователь в систему, не запретил ли он запись в свой терминал. В результате, перед отправкой сообщения нужно выполнить несколько проверок. Как видите, задача: «отправить сообщение», при ближайшем рассмотрении, оказалась задачей: «проверить возможность отправки сообщения, и, если нет препятствий, отправить его». Займёмся решением задачи, то есть — разработкой bash-скрипта. ### ▍Команды who и mesg Ядром скрипта являются несколько команд, которые мы ещё не обсуждали. Всё остальное должно быть вам знакомо по предыдущим материалам. Первое, что нам тут понадобится — команда `who`. Она позволяет узнать сведения о пользователях, работающих в системе. В простейшем виде её вызов выглядит так: ``` $ who ``` ![[5d04710940fc2750d0f14b5b14d197d1.png]] _Результаты вызова команды who_ В каждой строчке, которую выводит команда `who`, нас интересуют первых два показателя — имя пользователя и сведения о его терминале. По умолчанию запись в терминал разрешена, но пользователь может, с помощью команды `mesg`, запретить отправку ему сообщений. Таким образом, прежде чем пытаться что-то кому-то отправить, неплохо будет проверить, разрешена ли отправка сообщений. Если нужно узнать собственный статус, достаточно ввести команду `mesg` без параметров: ``` $ mesg ``` ![[6753391a3a4aeed1acae38862ac9d883.png]] _Команда mesg_ В данном случае команда вывела «is y», это значит, что пользователь, под которым мы работаем в системе, может принимать сообщения, отправленные в его терминал. В противном случае `mesg` выведет «is n». Для проверки того, разрешена ли отправка сообщений какому-то другому пользователю, можно использовать уже знакомую вам команду `who` с ключом `-T`: ``` $ who -T ``` При этом проверка возможна только для пользователей, которые вошли в систему. Если такая команда, после имени пользователя, выведет чёрточку (-), это означает, что пользователь запретил запись в свой терминал, то есть, сообщения ему отправлять нельзя. О том, что пользователю можно отправлять сообщения, говорит знак «плюс» (+). Если у вас приём сообщений отключён, а вы хотите позволить другим пользователям отправлять вам сообщения, можно воспользоваться такой командой: ``` $ mesg y ``` ![[f01eda2dc79b7040fd3d144aa06d676d.png]] _Включение приёма сообщений от других пользователей_ После включения приёма сообщений `mesg` возвращает «is y». Конечно, для обмена сообщениями нужны два пользователя, поэтому мы, после обычного входа в систему, подключились к компьютеру по ssh. Теперь можно поэкспериментировать. ### ▍Команда write Основной инструмент для обмена сообщениями между пользователями, вошедшими в систему — команда `write`. Если приём сообщений у пользователя разрешён, с помощью этой команды ему можно отправлять сообщения, используя его имя и сведения о терминале. Обратите внимание на то, что с помощью `write` можно отправлять сообщения пользователям, вошедшим в виртуальную консоль. Пользователи, которые работают в графическом окружении (KDE, Gnome, Cinnamon, и так далее), не могут получать подобные сообщения. Итак, мы, работая под пользователем `likegeeks`, инициируем сеанс связи с пользователем `testuser`, который работает в терминале `pts/1`, следующим образом: ``` $ write testuser pts/1 ``` ![[511a3970ab4a8546fd6ff1f07f87a307.png]] _Проверка возможности отправки сообщений и отправка сообщения_ После выполнения вышеуказанной команды перед нами окажется пустая строка, в которую нужно ввести первую строку сообщения. Нажав клавишу `ENTER`, мы можем ввести следующую строку сообщения. После того, как ввод текста завершён, окончить сеанс связи можно, воспользовавшись комбинацией клавиш `CTRL + D`, которая позволяет ввести [символ конца файла](https://habrahabr.ru/company/ruvds/blog/326826/). Вот что увидит в своём терминале пользователь, которому мы отправили сообщение. ![[a9551626918b606b20c58d9e30ed8ea0.png]] _Новое сообщение, пришедшее в терминал_ Получатель может понять от кого пришло сообщение, увидеть время, когда оно было отправлено. Обратите внимание на признак конца файла, `EOF`, расположенный в нижней части окна терминала. Он указывает на окончание текста сообщения. Полагаем, теперь у нас есть всё необходимое для того, чтобы автоматизировать отправку сообщений с помощью сценария командной строки. ### ▍Создание скрипта для отправки сообщений Прежде чем заниматься отправкой сообщений, нужно определить, вошёл ли интересующий нас пользователь в систему. Сделать это можно с помощью такой команды: ``` logged_on=$(who | grep -i -m 1 $1 | awk '{print $1}') ``` Здесь результаты работы команды `who` передаются команде `grep`. Ключ `-i` этой команды позволяет игнорировать регистр символов. Ключ `-m 1` включён в вызов команды на тот случай, если пользователь вошёл в систему несколько раз. Эта команда либо не выведет ничего, либо выведет имя пользователя (его мы укажем при вызове скрипта, оно попадёт в позиционную переменную `$1`), соответствующее первому найденному сеансу. Вывод `grep` мы передаём `awk`. Эта команда, опять же, либо не выведет ничего, либо выведет элемент, записанный в собственную переменную `$1`, то есть — имя пользователя. В итоге то, что получилось, попадает в переменную `logged_on`. Теперь надо проверить переменную l`ogged_on`, посмотреть, есть ли в ней что-нибудь: ``` if [ -z $logged_on ] then echo "$1 is not logged on." echo "Exit" exit fi ``` Если вы не вполне уверенно чувствуете себя, работая с конструкцией `if`, взгляните на [этот](https://habrahabr.ru/company/ruvds/blog/325522/) материал. Скрипт, содержащий вышеописанный код, сохраним в файле `senderscript` и вызовем, передав ему, в качестве параметра командной строки, имя пользователя `testuser`. ![[76150b96e45ce0d0fcb4eb0e536e0d4f.png]] _Проверка статуса пользователя_ Тут мы проверяем, является ли `logged_on` переменной с нулевой длиной. Если это так, нам сообщат о том, что в данный момент пользователь в систему не вошёл и скрипт завершит работу с помощью команды `exit`. В противном случае выполнение скрипта продолжится. ### ▍Проверка возможности записи в терминал пользователя Теперь надо проверить, принимает ли пользователь сообщения. Для этого понадобится такая конструкция, похожая на ту, которую мы использовали выше: ``` allowed=$(who -T | grep -i -m 1 $1 | awk '{print $2}') if [ $allowed != "+" ] then echo "$1 does not allowing messaging." echo "Exit" exit fi ``` ![[a3e08cf12f617fd3ff123265f8342fcc.png]] _Проверка возможности отправки сообщений пользователю_ Сначала мы вызываем команду `who` с ключом `-T`. В строке сведений о пользователе, который может принимать сообщения, окажется знак «плюс» (+), если же пользователь принимать сообщения не может — там будет чёрточка (-). То, что получилось после вызова `who`, передаётся `grep`, а потом — `awk`, формируя переменную `allowed`. Далее, используя условный оператор, мы проверяем то, что оказалось в переменной `allowed`. Если знака «плюс» в ней нет, сообщим о том, что отправка сообщений пользователю запрещена и завершим работу. В противном случае выполнение сценария продолжится. ### ▍Проверка правильности вызова скрипта Первым параметром скрипта является имя пользователя, которому мы хотим отправить сообщение. Вторым — текст сообщения, в данном случае — состоящий из одного слова. Для того, чтобы проверить, передано ли скрипту сообщение для отправки, воспользуемся таким кодом: ``` if [ -z $2 ] then echo "No message parameter included." echo "Exit" exit fi ``` ![[63b910246e010ac396f28324de51ce10.png]] _Проверка параметров командной строки, указанных при вызове скрипта_ Тут, если при вызове скрипта ему не было передано сообщение для отправки, мы сообщаем об этом и завершаем работу. В противном случае — идём дальше. ### ▍Получение сведений о терминале пользователя Прежде чем отправить пользователю сообщение, нужно получить сведения о терминале, в котором он работает и сохранить имя терминала в переменной. Делается это так: ``` terminal=$(who | grep -i -m 1 $1 | awk '{print $2}') ``` Теперь, после того, как все необходимые данные собраны, осталось лишь отправить сообщение: ``` echo $2 | write $logged_on $terminal ``` Вызов готового скрипта выглядит так: ``` $ ./senderscript testuser welcome ``` ![[a22e36dd5b5c6755fbba9efe3a8e09a7.png]] _Успешная отправка сообщения с помощью bash-скрипта_ Как видно, всё работает как надо. Однако, с помощью такого сценария можно отправлять лишь сообщения, состоящие из одного слова. Хорошо бы получить возможность отправлять более длинные сообщения. ### ▍Отправка длинных сообщений Попробуем вызвать сценарий `senderscript`, передав ему сообщение, состоящее из нескольких слов: ``` $ ./senderscript likegeeks welcome to shell scripting ``` ![[6a8062728f8d848966abbd3bcf95a5ed.png]] _Попытка отправки длинного сообщения_ Как видно, отправлено было лишь первое слово. Всё дело в том, что каждое слово сообщения воспринимается внутри скрипта как отдельная позиционная переменная. Для того, чтобы получить возможность отправки длинных сообщений, обработаем параметры командной строки, переданные сценарию, воспользовавшись [командой](https://habrahabr.ru/company/ruvds/blog/326328/) `shift` и циклом `while`. ``` shift while [ -n "$1" ] do whole_message=$whole_message' '$1 shift done ``` После этого, в команде отправки сообщения, воспользуемся, вместо применяемой ранее позиционной переменной `$2`, переменной `whole_message`: ``` echo $whole_message | write $logged_on $terminal ``` Вот полный текст сценария: ``` #!/bin/bash logged_on=$(who | grep -i -m 1 $1 | awk '{print $1}') if [ -z $logged_on ] then echo "$1 is not logged on." echo "Exit" exit fi allowed=$(who -T | grep -i -m 1 $1 | awk '{print $2}') if [ $allowed != "+" ] then echo "$1 does not allowing messaging." echo "Exit" exit fi if [ -z $2 ] then echo "No message parameter included." echo "Exit" exit fi terminal=$(who | grep -i -m 1 $1 | awk '{print $2}') shift while [ -n "$1" ] do whole_message=$whole_message' '$1 shift done echo $whole_message | write $logged_on $terminal ``` Испытаем его: ``` $ ./senderscript likegeeks welcome to shell scripting ``` ![[c85623c06f86712b250620ce794d7e6e.png]] _Успешная отправка длинного сообщения:_ Длинное сообщение успешно дошло до адресата. Теперь рассмотрим следующий пример. ## Скрипт для мониторинга дискового пространства Сейчас мы собираемся создать сценарий командной строки, который предназначен для поиска в заданных директориях первой десятки папок, на которые приходится больше всего дискового пространства. В этом нам поможет [команда](https://likegeeks.com/main-linux-commands-easy-guide/#du-Command) `du`, которая выводит сведения о том, сколько места на диске занимают файлы и папки. По умолчанию она выводит сведения лишь о директориях, с ключом `-a` в отчёт попадают и отдельные файлы. Её ключ `-s` позволяет вывести сведения о размерах директорий. Эта команда позволяет, например, узнать объём дискового пространства, который занимают данные некоего пользователя. Вот как выглядит вызов этой команды: ``` $ du -s /var/log/ ``` Для наших целей лучше подойдёт ключ `-S` (заглавная S), так как он позволяет получить сведения как по корневой папке, так и по вложенным в неё директориям: ``` $ du -S /var/log/ ``` ![[42bb5e8caec726f7d312cf1f9df25a49.png]] _Вызов команды du с ключами -s и -S_ Нам нужно найти директории, на которые приходится больше всего дискового пространства, поэтому список, который выдаёт `du`, надо отсортировать, воспользовавшись командой `sort`: ``` $ du -S /var/log/ | sort -rn ``` ![[cffef29a8ee74eb8992799d26d56fa16.png]] _Отсортированный список объектов_ Ключ `-n` указывает команде на то, что нужна числовая сортировка, ключ `-r —` на обратный порядок сортировки (самое большое число окажется в начале списка). Полученные данные вполне подходят для наших целей. Для того, чтобы ограничить полученный список первыми десятью записями, воспользуемся [потоковым редактором](https://habrahabr.ru/company/ruvds/blog/327530/) `sed`, который позволит удалить из полученного списка все строки, начиная с одиннадцатой. Следующий шаг — добавить к каждой полученной строке её номер. Тут также поможет `sed`, а именно — его команда `N`: ``` sed '{11,$D; =}' | sed 'N; s/\n/ /' | ``` Приведём полученные данные в порядок, воспользовавшись `awk`. Передадим `awk` то, что получилось после обработки данных с помощью `sed`, применив, как и в других случаях, конвейер, и выведем полученные данные с помощью команды `printf`: ``` awk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}' ``` В начале строки выводится её номер, потом идёт двоеточие и знак табуляции, далее — объём дискового пространства, следом — ещё один знак табуляции и имя папки. Соберём вместе всё то, о чём мы говорили: ``` $ du -S /var/log/ | sort -rn | sed '{11,$D; =}' | sed 'N; s/\n/ /' | awk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}' ``` ![[2857bc4fb85059664b25f99ee26ae472.png]] _Вывод сведений о дисковом пространстве_ Для того, чтобы повысить эффективность работы скрипта, код которого вы совсем скоро увидите, реализуем возможность получения данных сразу по нескольким директориям. Для этого создадим переменную `MY_DIRECTORIES` и внесём в неё список интересующих нас директорий: ``` MY_DIRECTORIES="/home /var/log" ``` Переберём список с помощью цикла `for` и вызовем вышеописанную последовательность команд для каждого элемента списка. Вот что получилось в результате: ``` #!/bin/bash MY_DIRECTORIES="/home /var/log" echo "Top Ten Disk Space Usage" for DIR in $MY_DIRECTORIES do echo "The $DIR Directory:" du -S $DIR 2>/dev/null | sort -rn | sed '{11,$D; =}' | sed 'N; s/\n/ /' | awk '{printf $1 ":" "\t" $2 "\t" $3 "\n"}' done exit ``` ![[c1868fd4a7092a83026dfee3032b1a80.png]] _Получение сведений о нескольких директориях_ Как видите, скрипт выводит, в виде удобного списка, сведения о директориях, список которых хранится в `MY_DIRECTORIES`. Команду `du` в этом скрипте можно вызвать с другими ключами, полученный список объектов вполне можно отфильтровать, в целом — тут открывается широкий простор для самостоятельных экспериментов. В результате, вместо работы со списком папок, можно, например, найти самые большие файлы с расширением `.log,` или реализовать более сложный алгоритм поиска самых больших (или самых маленьких) файлов и папок. ## Итоги Сегодня мы подробно разобрали пару примеров разработки скриптов. Тут хотелось бы напомнить, что наша главная цель — не в том, чтобы написать скрипт для отправки сообщений с помощью команды `write`, или сценарий, который помогает в поиске файлов и папок, занимающих много места на диске, а в описании самого процесса разработки. Освоив эти примеры, поэкспериментировав с ними, возможно — дополнив их или полностью переработав, вы научитесь чему-то новому, улучшите свои навыки разработки bash-скриптов. На сегодня это всё. В следующий раз поговорим об автоматизации работы с интерактивными утилитами с помощью expect. Уважаемые читатели! Есть ли у вас на примете несложные (а может быть и сложные, но понятные) bash-скрипты, разбор которых будет полезен новичкам?