Bash-скрипты — эффективное решение для автоматизации рутинных задач, но не всегда самое простое. Объемные сценарии характеризуются низкой производительностью и сложны для чтения. В этой статье мы рассмотрим, как оптимизировать работу, упростить с помощью утилит sed и awk и не совершать очевидных ошибок в написании скриптов.
Настройка выполнения скриптов
Управление процессами в Linux увеличивает коэффициент полезного использования ЦП, памяти, устройств ввода-вывода и других составляющих системы. Рассмотрим, как применить эти принципы и команды при запуске bash-скриптов в работу.
Скорость работы сценария зависит от количества операций и их «энергозатратности» для ресурсов системы. Проблема в том, что во время его выполнения системный администратор не может использовать консоль. Как решить эту задачу?
Использовать терминальные мультиплексоры: screen или более продвинутый tmux, о которых слышали даже «новички» в Unix-среде. Программы позволяют разделить терминал, создав несколько окон и поддерживая несколько сессий одновременно.
Перемещать процесс выполнения скрипта в фоновый режим. Если скрипт еще не запущен, то добавьте амперсанд & в конце команды
$ ./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 &
Команда disown -h вводится после запуска или перевода скрипта в фоновый режим. Выполнение сценария будет продолжаться и после закрытия сессии терминала.
$ ./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.
Направить stdout скрипта в конкретный файл можно с помощью такой команды
$ ./slurmscript > slurmfile &
Не менее полезные инструменты для управления скриптами — at, cron и anacron. Утилиты автоматизируют любые процессы, в том числе запуск сценариев:
At — команда однократно задающая дату и время начала процесса.
# время дата
$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 или другой утилиты, то есть выводить один и тот же результат. В этом случае отдайте предпочтение встроенным командам или команде, содержащей меньше процессов.
И как не надо:
в именах переменных не должно быть служебных символов bash и они не должны совпадать с именами функций или специальными словами:
"var-3=19" или "time=19"
команда, для запуска которой пользователю не хватает прав доступа, не будет работать и в запущенном им сценарии;
перенос файла со сценарием, сохраненного в Windows, требует решения проблемы деления строк. В MS-DOS строка завершается двумя символами:
#!/bin/bash\r\n
а в Linux — одним:
#!/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
Комментарии (14)
ZeroBot-Dot
14.07.2022 19:15+1Спасибо, хотя и не знал только про &
Но всегда полезно освежить знания :)
ALiEN175
14.07.2022 19:17+5И вообще, хоть бы через банальный shellcheck прогнали, прежде чем выкладывать на всеобщее обозрение.
Shellcheckshellcheck test.sh In test.sh line 11: if [ ! -d $archivedir ] ; then ^---------^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: if [ ! -d "$archivedir" ] ; then In test.sh line 15: cd $archivedir ^------------^ SC2164 (warning): Use 'cd ... || exit' or 'cd ... || return' in case cd fails. ^---------^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: cd "$archivedir" || exit In test.sh line 22: ls -FC | sed -e 's/\([[:digit:]][[:digit:]]\.\)\{5\}//g' \ ^----^ SC2012 (info): Use find instead of ls to better handle non-alphanumeric filenames. In test.sh line 27: matches="$(ls -d *"$1" 2> /dev/null | wc -l)" ^----------------------^ SC2012 (info): Use find instead of ls to better handle non-alphanumeric filenames. ^-- SC2035 (info): Use ./*glob* or -- *glob* so names with dashes won't become options. In test.sh line 28: if [ $matches -eq 0 ] ; then ^------^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: if [ "$matches" -eq 0 ] ; then In test.sh line 33: if [ $matches -gt 1 ] ; then ^------^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: if [ "$matches" -gt 1 ] ; then In test.sh line 36: for name in $(ls -td *"$1") ^-------------^ SC2045 (warning): Iterating over ls output is fragile. Use globs. ^-- SC2035 (info): Use ./*glob* or -- *glob* so names with dashes won't become options. In test.sh line 38: datetime="$(echo $name | cut -c1-14| \ ^---^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: datetime="$(echo "$name" | cut -c1-14| \ In test.sh line 42: filename="$(echo $name | cut -c16-)" ^---^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: filename="$(echo "$name" | cut -c16-)" In test.sh line 43: if [ -d $name ] ; then ^---^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: if [ -d "$name" ] ; then In test.sh line 44: filecount="$(ls $name | wc -l | sed 's/[^[:digit:]]//g')" ^------^ SC2012 (info): Use find instead of ls to better handle non-alphanumeric filenames. ^---^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: filecount="$(ls "$name" | wc -l | sed 's/[^[:digit:]]//g')" In test.sh line 48: size="$(ls -sdk1 $name | awk '{print $1}')" ^------------^ SC2012 (info): Use find instead of ls to better handle non-alphanumeric filenames. ^---^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: size="$(ls -sdk1 "$name" | awk '{print $1}')" In test.sh line 51: index=$(( $index + 1)) ^----^ SC2004 (style): $/${} is unnecessary on arithmetic variables. In test.sh line 55: read desired ^--^ SC2162 (info): read without -r will mangle backslashes. In test.sh line 56: if [ ! -z "$(echo $desired | sed 's/[[:digit:]]//g')" ] ; then ^-- SC2236 (style): Use -n instead of ! -z. ^-- SC2001 (style): See if you can use ${variable//search/replace} instead. ^------^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: if [ ! -z "$(echo "$desired" | sed 's/[[:digit:]]//g')" ] ; then In test.sh line 61: if [ ${desired:=1} -ge $index ] ; then ^-----------^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: if [ "${desired:=1}" -ge $index ] ; then In test.sh line 73: restore="$(ls -td1 *"$1" | sed -n "${desired}p")" ^-----------^ SC2012 (info): Use find instead of ls to better handle non-alphanumeric filenames. ^-- SC2035 (info): Use ./*glob* or -- *glob* so names with dashes won't become options. In test.sh line 85: read answer ^--^ SC2162 (info): read without -r will mangle backslashes. In test.sh line 87: if [ ${answer:=y} = "y" ] ; then ^----------^ SC2086 (info): Double quote to prevent globbing and word splitting. Did you mean: if [ "${answer:=y}" = "y" ] ; then In test.sh line 88: $realrm -rf *"$1" ^-- SC2035 (info): Use ./*glob* or -- *glob* so names with dashes won't become options. In test.sh line 98: restore="$(ls -d *"$1")" ^-- SC2035 (info): Use ./*glob* or -- *glob* so names with dashes won't become options. For more information: https://www.shellcheck.net/wiki/SC2045 -- Iterating over ls output is fragi... https://www.shellcheck.net/wiki/SC2164 -- Use 'cd ... || exit' or 'cd ... |... https://www.shellcheck.net/wiki/SC2012 -- Use find instead of ls to better ...
dmitryvolochaev
14.07.2022 22:50перенос файла со сценарием, сохраненного в Windows, требует решения проблемы деления строк
Это да. Главное, ругается на синтаксическую ошибку на пустом месте. Если заранее не знать об этой фиче баша, можно вечно искать ошибку там, где ее нет - в видимом тексте.
Я не знаю ни одного другого языка программирования, для которого были бы важны концы строк. Обычно и компиляторы, и редакторы на обеих платформах понимают обе кодировки.
Если в вашем проекте используются баш-скрипты, и часть людей редактирует их под виндой, в системе контроля версий надо обязательно настраивать преобразование концов строк при коммите.
ALiEN175
15.07.2022 01:19-2Какие концы строк? Если вы скрипты для *nix пишете на CP1251 — ССЗБ.
TonyKentnarEarth
15.07.2022 19:31CRLF / LF
ALiEN175
17.07.2022 02:22Я в курсе про CRFL. Писать bash скрипты в блокноте windows немножко неправильно.
Покажите мне linux редактор, который по-умолчанию сохраняет в CRLF?
13werwolf13
15.07.2022 06:22на самом деле достаточно закопать стюардессу
в современной версии win даже банальный блокнот умеет переключаться на unix и использовать правильные переносы строк, а различные ide и подавно
да и если в вашем проекте баш скрипты редачит человек сидящий за виндой очень может быть что у вас изначально проблемы с распределением обязанностей...
vviz
15.07.2022 09:55Проблема в том, что во время его выполнения системный администратор не может использовать консоль
А как же переключение между консолями - Alt+Fx/Alt+Ctrl+Fx ?
ALiEN175
и прочие ls pipe — сразу бить по рукам за такое. ls предназначен для чтения человеком, а не парсинга.
Sau
Не хватает ответа на вопрос - а вывод какой команды предназначен для парсинга?
ALiEN175
find -exec ... ; for file in ./dir ...