Как повысить эффективность Bash-скриптов

Bash-скрипты — эффективное решение для автоматизации рутинных задач, но не всегда самое простое. Объемные сценарии характеризуются низкой производительностью и сложны для чтения. В этой статье мы рассмотрим, как оптимизировать работу, упростить с помощью утилит sed и awk и не совершать очевидных ошибок в написании скриптов. 

9b0fe9aa46488c8f6a62466a7a189e88.jpg

Настройка выполнения скриптов

Управление процессами в Linux увеличивает коэффициент полезного использования ЦП, памяти, устройств ввода-вывода и других составляющих системы. Рассмотрим, как применить эти принципы и команды при запуске bash-скриптов в работу. 

Скорость работы сценария зависит от количества операций и их «энергозатратности» для ресурсов системы. Проблема в том, что во время его выполнения системный администратор не может использовать консоль.  Как решить эту задачу?  

  1. Использовать терминальные мультиплексоры: screen или более продвинутый tmux, о которых слышали даже «новички» в Unix-среде. Программы позволяют разделить терминал, создав несколько окон и поддерживая несколько сессий одновременно. 

  2. Перемещать процесс выполнения  скрипта в фоновый режим. Если скрипт еще не запущен, то добавьте амперсанд & в конце команды

$ ./slurmscript &

Когда скрипт уже выполняется, приостановите его сочетанием клавиш Ctrl+Z и используйте команду $ bg для продолжения его работы, но уже в фоновом режиме. А чтобы вернуть скрипт на «передний план», введите после Ctrl+Z $ fg. Без указания дополнительных параметров опция будет применяться к текущему выполняемому заданию. Список всех задач оболочки отображается командой jobs:

$ jobs
 
[1]      Stopped                         ./slurmscript
[2]      Running                         ./slurmscript > slurmfile &
 
$ fg 1      #перевод в активный режим задачи №1 в списке

 Несмотря на то, что сценарий работает в фоновом режиме, выходные данные выдаются на экран терминала. А выполнение скрипта остановится при отключении от сервера. Чтобы этого избежать:  

  • Команда nohup нарушит прямую связь между выполняемым скриптом и терминалом. Сценарий не остановится в момент выхода из сессии, а вывод данных запишется в файл nohup.log.

$ nohup ./slurmscript &

Если скрипт не выводит никаких данных, файл останется пустым. Перенаправьте вывод в /dev/null, чтобы файл не был создан автоматически.

$ nohup ./slurmscript >/dev/null &
$ ./slurmscript &   +                  
[1]   4545
$jobs                                
[1] +     Running                      ./slurmscript &
[2] -     Running                      ping slurm.io & >/dev/null
                        
$disown -h
$jobs 
[1] +     Running                      ping slurm.io & >/dev/null

Disown -h можно использовать без дополнительных параметров, когда она применяется для текущего процесса. В других случаях, необходимо ввести номер строки в списке или pid (идентификатор) процесса. В результате выполняемый скрипт исчез из списка заданий оболочки и не получит сигнала о выходе из терминала. Чтобы убедиться, что процесс продолжается, используйте команды ps или top.  

$ ./slurmscript > slurmfile &

Не менее полезные инструменты для управления скриптами — at, cron и anacron. Утилиты автоматизируют любые процессы, в том числе запуск сценариев:

  # время дата
$at -f /home/slurm/slurm1 10:30 01092022

Ключ -f необходимо использовать для указания файла утилите, вместо конкретного процесса. Команда at распознает разнообразные форматы указания даты: $at 01:00 PM (час дня), $at now+10 minutes (через 10 минут), $at 09.00 AM next month (ровно через месяц в 10 утра), $at tomorrow (через 24 часа). Для завершения работы с установкой времени, нажмите Ctrl+D, просмотреть заданные параметры утилиты — $atq, а удалить — $atrm c указанием номера в списке.

  • Cron — позволяет многократно выполнять процессы по заданному расписанию. Установить дату и время для скриптов можно в конфигурационном файле crontab. Для работы с файлом используются три основные команды: $crontab -l (просмотреть все задачи cron), $crontab -r (удалить все записи) и $crontab -e (внести изменения в задачи для cron). Редактирование планировщика строится на заполнении данных пяти полей временного интервала и поля, указывающего полный путь к скрипту:

#мин  чаcы   день   месяц   день недели             	скрипт
  *    *      *      *       *   	              /home/slurm/slurm1

Допустимые значения для категории минут — от нуля до 59, часов — от 0 до 23, дней месяца — от 1 до 31, месяцев — от 1 до 12 и дней недели — от 0 до 6 или от 1 до 7 (в некоторых версиях воскресенье обозначается нулем, а в других — семеркой). Если ни одно поле не будет заполнено, то планировщик станет запускать скрипт каждую минуту, каждого часа и т.д.

0    8,20     *      */2     1-5                  /home/slurm/slurm1
  • Согласно настройкам скрипт slurm1 будет запускаться в фоновом режиме в восемь утра и восемь вечера (8, 20) ноль минут (0), независимо от числа (*), но только с пн по пт (1–5) и каждые два месяца (*/2).

  • Anacron — отличается от Cron тем, что изменения в конфигурационный файл может вносить только root, учитываются невыполненные задачи во время отключения компьютера, вместо точного времени можно задать только интервал. Для настройки периодичности заданий используются те же команды, что и для Cron.

Sed и awk

Инструменты обработки текста значительно расширяют возможности оболочки bash. Но с командами sed и awk можно не только редактировать вывод и файлы, включая сами скрипты. Утилиты служат наиболее эффективным решением некоторых задач автоматизации процессов. 

Sed — потоковый редактор файлов, позволяющий сэкономить время на выполнении простых функций: удаление, замена, вставка текста. Как это работает? Утилите передается информация в виде набора условий. Она по очереди «читает» строки в файле или файлах и применяет заданные правила: sed »[область применения] [опция] / [шаблон для изменения] / [новый шаблон] / [w обновленный файл]»  [исходный файл].

Например: заменить в третьей строке файла Slurm выражение «New course» на «Linux Mega» и сохранить изменения в копии исходного файла slurm1

$ sed '3s/new course/linux mega/w slurm1' slurm

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

Область применения заданных правил можно обозначить конкретной строкой, как в примере, диапазоном строк (2,3 — вторая и третья; 4,$ — с четвертой строки и до конца файла) или как полный текст (ключ g после [новый шаблон] — 

$ sed 's/new course/linux mega/g' slurm)

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

Опции, которые могут пригодиться для создания скрипта: s — замена шаблона выражений, y — замена шаблона символов, d — удаление строк, i — вставка перед указанной строкой, а — вставка текста после указанной строки. Команда может содержать несколько правил внесения изменений, перечисленных через точку с запятой. Чтобы она сработала необходимо добавить ключ -e перед «областью применения»:

$sed -e '/^#/d; /^$/d' slurm1

Sed читает первую строку файла slurm1 в поисках совпадений с одним или всеми заданными в слешах (//) шаблонами. Набор символов »^#» обозначает строки, начинающиеся (^) со знака #, то есть комментарии. А символы »^$» — пустые строки, в которых нет никаких знаков от начала строки (^) до ее окончания ($). Если первая строка подходит под заданные параметры, sed анализирует, входит ли строка в область применения и выполняет необходимое действие, то есть удаляет ее, а затем обращается к следующей строке.

Эту команду можно ввести разными способами: перечислив шаблоны через запятую перед «d» или отделяя каждую опцию переходом на новую строку вместо »;». Но наиболее часто используемый вариант написания в скриптах:

 $sed -e '/^#/d' -e '/^$/d' slurm1 

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

Awk — язык программирования, синтаксис которого напоминает языки C и Perl. Хотя awk работает по тому же «построчному» принципу, но значительно превосходит sed по функциональным возможностям. При написании bash-скриптов инструмент удобно использовать для работы со структурированными данными, так как awk воспринимает поля (область текста, отделенную пробелами или табуляцией), переменные, арифметические функции и др.

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

Если выстроить параметры по аналогии с примером для утилиты sed, то команда будет выглядеть так: $awk [опция] »{[функция] [шаблон для изменения]}» [исходный файл]. Подобная схема получилась условной, так как в [опциях] и [функциях] могут использоваться дополнительные ключи, переменные, циклы, операторы обработки текста, например, сравнения и совпадения. Ниже отрывок из скрипта для выявления пользователей, расходующих большой объем дискового пространства:  

for name in $(cut -d: -f1,3 /etc/passwd | 
awk -F: '$2 > 99 {print $1}') 
do 
/bin/echo -en "User $name exceeds disk quota.\n"
done

Где -F: — опция для разделения текста (двоеточие — пример знака, по которому строки делятся на поля), $2 и $1 — обозначение столбца данных, print — функция для вывода измененных данных, а /etc/password — имя исходного файла. То есть утилита просматривает данные второго столбца в файле, уже отредактированном командой cut, сравнивает значения с числом 99 и выводит данные первого столбца, то есть имена пользователей из файла /etc/passwd.

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

$ echo "Moscow is the capital of GB" | awk '{$1="London"; print $0}'
London is the capital of GB

Сначала утилита заменяет текст первого поля на значение в кавычках «London», а затем выводит всю строку ($0) измененного комментария.

Awk использует множество опций. Помимо самой распространенной -F: могут потребоваться -v var=value — задать переменную, -o — вывести обработанный текст в файл, -f — указать файл сценария для выполнения.

Процесс работы awk делиться на 3 этапа: до, обработка текста и после нее. Используя опцию BEGIN, можно задать переменные или вставить текст до отображения данных:

$ awk 'BEGIN {FIELDWIDTHS="1 3 5"} {print $1, $2, $3}' newfile

Прежде, чем вывести числовые значения из файла, утилита структурирует их согласно заданной опции FIELDWIDTHS по количеству знаков. Тогда в первом столбце будет показана 1 цифра из строки, через пробел еще 3 цифры и потом 5 оставшихся. А с помощью опции END получится вывести результат выполненного действия:

$ awk 'END { print "Course consists of", NR, "modules"}' file.txt

Например, в основном процессе обработки были внесены изменения количества строк в файле, а после ее завершения необходимо вывести это количество (опция NR) в удобочитаемом варианте.

Кроме собственных опций и ключей утилиты sed и awk поддерживают использование регулярных выражений. Так можно задать команде awk вывод строк, содержащих цифры, специальным набором символов [[: digit:]]. 

awk '/[[:digit:]]/{print $0}'

И более сложный пример для утилиты sed — замена первых пяти вхождений конструкций «цифра-цифра-точка» из каждой строки файла:

sed -e 's/\([[:digit:]][[:digit:]]\.\)\{5\}//g' \
       -e 's/^/ /'

Такое нагромождение знаков вызвано необходимостью выделить (/\ или ) метасимволы ({, [, ^ и т.д.), чтобы оболочка верно их прочитала.   

Bash-скрипт Unrm

На профессиональных форумах часто ведутся споры о выборе правильного инструмента для решения задачи. Некоторые специалисты считают, что bash-скрипты должны ограничиваться 10–15 строками, другие — одним циклом или одной функцией. Тогда как, третьи пишут сложные многоуровневые скрипты. Вот один из примеров реально работающих сценариев для ознакомления, часть действий которого проще и эффективнее организовать функциями утилит sed и awk.

Bash-скрипт для восстановления резервных копий.

#!/bin/bash
 
# Unrm — находит в архиве резервных копий удаленных файлов тот, который запрашивается пользователем. В случае наличия нескольких резервных копий одного файла, выводит их списком с сортировкой по времени.
 
archivedir="$HOME/.deleted-files"
realrm="$(which rm)"
move="$(which mv)"
 
dest=$(pwd)
 
if [ ! -d $archivedir ] ; then
   echo "$0: No deleted files directory: nothing to unrm" >&2
   exit 1
fi
 cd $archivedir
 
if [ $# -eq 0 ] ; then
   echo "Contents of your deleted files archive (sorted by date):"


# sed используется с регулярным выражением и удаляет заданные шаблоны символов из вывода каждой строки команды ls. Такая информация не имеет ценности и загромождает вывод списка 
   ls -FC | sed -e 's/\([[:digit:]][[:digit:]]\.\)\{5\}//g' \
 -e 's/^/ /'
    exit 0
 fi
 
matches="$(ls -d *"$1" 2> /dev/null | wc -l)"
  if [ $matches -eq 0 ] ; then
     echo "No match for \"$1\" in the deleted file archive." >&2
     exit 1
 fi
 
if [ $matches -gt 1 ] ; then
    echo "More than one file or directory match in the archive:"
    index=1
   for name in $(ls -td *"$1")
   do
  	 datetime="$(echo $name | cut -c1-14| \

# awk заменяет префикс имени файла на дату удаления исходного файла
        	awk -F. '{ print $5"/"$4" at "$3":"$2":"$1 }')"
 	 filename="$(echo $name | cut -c16-)"
 	 if [ -d $name ] ; then
    	filecount="$(ls $name | wc -l | sed 's/[^[:digit:]]//g')"
    	echo " $index) $filename (contents = ${filecount} items," \
     	         " deleted = $datetime)"
 	 else
         	size="$(ls -sdk1 $name | awk '{print $1}')"
        	 echo " $index) $filename (size = ${size}Kb, deleted = $datetime)"
	 fi
     index=$(( $index + 1))
done
echo ""
/bin/echo -n "Which version of $1 should I restore ('0' to quit)? [1] : "
read desired
if [ ! -z "$(echo $desired | sed 's/[[:digit:]]//g')" ] ; then
   echo "$0: Restore canceled by user: invalid input." >&2
   exit 1
fi
 
 if [ ${desired:=1} -ge $index ] ; then
	 echo "$0: Restore canceled by user: index value too big." >&2
	 exit 1
 fi
 
 if [ $desired -lt 1 ] ; then
	 echo "$0: Restore canceled by user." >&2
	 exit 1
 fi


# sed служит для извлечения из stdout ключами -n и p строки, в которой указана искомая копия файла 
restore="$(ls -td1 *"$1" | sed -n "${desired}p")"
 
if [ -e "$dest/$1" ] ; then
	  echo "\"$1\" already exists in this directory. Cannot overwrite." >&2
 	 exit 1
 fi
 
/bin/echo -n "Restoring file \"$1\" ..."
 $move "$restore" "$dest/$1"
 echo "done."
 
/bin/echo -n "Delete the additional copies of this file? [y] "
read answer
 
  if [ ${answer:=y} = "y" ] ; then
  	$realrm -rf *"$1"
      echo "Deleted."
else
     echo "Additional copies retained."
   fi
else
    if [ -e "$dest/$1" ] ; then
   	echo "\"$1\" already exists in this directory. Cannot overwrite." >&2
   	exit 1
    fi
   restore="$(ls -d *"$1")"
   /bin/echo -n "Restoring file \"$1\" ... "
   $move "$restore" "$dest/$1"
   echo "Done."
 fi
 exit 0

Bash-скрипты: что такое хорошо и что такое плохо

Главный совет при создании bash-скриптов — подумать прежде, чем писать. Чтобы сценарий выполнялся корректно, был удобен для дальнейшего использования или масштабирования, необходимо определиться с его структурой и логикой построения до начала работы. 

Как надо:

  • научитесь пользоваться константами при вводе одинаковых значений или утилитой sed для удобства внесения изменений в скрипт в будущем;

  • дробите сложные сценарии на небольшие части и используйте функции;

  • задавайте рациональные имена переменным и функциям;

  • вносите комментарии, чтобы смысл команды не забылся, а сценарий мог обслуживать не только автор;

  • определитесь с регистром, типами скобок, сделайте стиль единообразным;  

  • не используйте ресурсозатратные операции внутри циклов, например, find;

  • утилиты могут эмулировать функции встроенной команды bash или другой утилиты, то есть выводить один и тот же результат. В этом случае отдайте предпочтение встроенным командам или команде, содержащей меньше процессов.

И как не надо:

"var-3=19" или "time=19"
  • команда, для запуска которой пользователю не хватает прав доступа, не будет работать и в запущенном им сценарии;         

  • перенос файла со сценарием, сохраненного в Windows, требует решения проблемы деления строк. В MS-DOS строка завершается двумя символами:

#!/bin/bash\r\n
#!/bin/bash\n

Удалить лишнюю -r можно с помощью редактора vim или утилит dos2unix и unix2dos.

  • дополнительные пробелы, свойственные языку С foo = bar

  • символы && и || не эмулируют команду if… then… else… fi в большинстве случаев;

  • искать опечатки в переменных через команду set -u (лучше использовать sell check).

Краткий обзор полезных функций для совершенствования сценариев — завершен.

Для тех, кто до конца не разобрался или хочет узнать больше полезных советов

28 июля 2022 стартует практический курс Southbridge + Слёрм «Администрирование Linux. Mega». Опыт автора программы инженера Southbridge Платона Платонова не ограничивается написанием bash-скриптов, а подкрепляется ни единым кейсом «best of the best» practice.  

Вас ждут: 5 недель, 9 блоков, 12 часов теории, 48 часов практики на стендах, 80 lvl владения OC по окончанию. Углубьте свои знания работы с Linux за месяц.     

Смотреть программу и занять местечко в потоке 28 июля: https://slurm.club/3yEGiAf

© Habrahabr.ru