Введение в Bash Shell

Сценарии оболочки — наука сама по себе. Не вдаваясь в подробности всего, что происходит «под капотом», вы узнаете, как применять базовые элементы для написания собственных скриптов, и анализировать, что происходит в сторонних сценариях оболочки.

Понимание основных элементов сценариев оболочки

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

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

Ниже показан очень простой скрипт:
#!/bin/bash
#
# #This is a script that greets the world
# Usage: ./hello
clear
echo hello world
exit 0

Здесь содержатся несколько элементов, которые должны использоваться во всех скриптах. Для начала, есть shebang — это строка #!/bin/bash. Когда скрипт запускается из родительской оболочки, он открывает подоболочку, в которой и выполняются команды, указанные в скрипте. Эти команды могут быть интерпретированы различными способами. Для того, чтобы понять, как именно они должны интерпретироваться, используется shebang. В примере выше shebang ясно даёт понять, что скрипт должен выполняться оболочкой bash.

Также могут быть указаны другие оболочки. Например, если ваш скрипт содержит код на языке Perl, shebang должен быть #!/usr/bin/perl. Начинать сценарий с shebang является хорошей практикой; если он опущен, код сценария будет выполняться такой же оболочкой, которая используется для запуска скрипта.

Сразу после shebang расположена часть, объясняющая, о чем сценарий. Несколько строк комментариев в начале каждого сценария — хорошая идея. В коротком скрипте часто очевидно, что он делает, но когда сценарий становится длиннее, и по мере того, как всё больше людей вовлекаются в его написание и поддержку, становится менее понятно, что авторы намереваются сделать. Чтобы избежать такой ситуации, убедитесь, что вы добавили строки комментариев, начиная каждую символом #. Комментарии могут быть не только в первых строках, но и в начале каждого подраздела сценария. Это наверняка поможет, если вы прочитаете свой скрипт через несколько месяцев!

Вы также можете комментировать не только подразделы, но и отдельные строки.

Независимо от того, в какой позиции он используется, всё от символа # и до конца строки является комментарием.

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

В конце скрипта я включил инструкцию exit 0. Оператор выхода сообщает родительской оболочке, был ли сценарий успешным. Состояние выхода последней команды в сценарии является состоянием выхода самого сценария, если только команда exit 0 не используется в конце сценария. Полезно знать, что вы можете работать с exit, чтобы сообщить родительской оболочке, как все прошло. 
Введённая в родительской оболочке, команда echo $? позволяет запросить состояние выхода последнего запущенного сценария.
После создания сценария убедитесь, что он может быть выполнен. Самый распространённый способ сделать это — применить к нему бит выполнения. Итак, если имя файла скрипта — hello, используйте команду chmod +x ./hello для того, чтобы сделать его исполняемым. Сценарий также может быть выполнен в качестве аргумента команды bash. В этом случае введите bash ./hello для запуска скрипта hello. Если сценарий запускается как аргумент команды bash, файл скрипта не обязательно должен быть исполняемым.

По сути, вы можете хранить скрипт в любом месте, но если вы собираетесь хранить его в каталоге, который не включён в переменную $PATH, вам нужно выполнить его с ./ перед именем скрипта. Введите ./hello для того, чтобы запустить сценарий, или поместите его в стандартный каталог, который включён в переменную $PATH, например, /usr/local/bin.

Также вы можете разместить сценарий в каталоге /bin, после чего в любом месте файловой системы просто ввести имя файла, и сценарий выполнится.



Пример
Командой vi /bin/datetimeсоздадим в каталоге /bin файл с именем datetime. В созданный файл вставим это содержимое:
#!/bin/bash
# выводит дату и время. а так же список пользователей, которые залогинены в данный момент

date
who

Сохранив файл, введите chmod +x /bin/datetime, чтобы дать файлу права на выполнение. Перейдите, к примеру, в домашний каталог cd ~ и просто введите datetime.
[root@localhost ~]# datetime
Sat Sep 28 00:33:41 EDT 2019
root     tty1         2019-09-25 20:28
root     pts/0        2019-09-27 20:07 (comp.corp.domain.ru)

Использование переменных и входных данных

bash-скрипты — это гораздо больше, чем просто список команд, которые выполняются последовательно. Одна из приятных сторон скриптов заключается в том, что они могут работать с переменными и входными данными, чтобы сделать скрипт гибким. В этом разделе вы узнаете, как с ними работать.

Использование позиционных параметров

При запуске скрипта можно использовать аргументы. Аргумент — это всё, что вы помещаете за командой сценария. Аргументы могут быть использованы для того, чтобы сделать скрипт более гибким. Возьмём команду useradd lisa. В этом примере команда — это useradd, а её аргумент — lisa — указывает, что нужно сделать. В результате выполнения такой команды должен быть создан пользователь с именем lisa. В тексте сценария первый аргумент обозначается $1, второй аргумент — $2 и т. д. Листинг 1 показывает, как можно использовать аргументы. Попробуйте запустить этот код, указав в качестве параметров любые имена пользователей.

Листинг 1
#!/bin/bash
# run this script with a few arguments
echo The first argument is $1
echo The second argument is $2
echo The third argument is $3

Под параметрами подразумевается ввод данных перед запуском скрипта. В данном случае в качестве параметров после имени скрипта argument я указал lisa, lori и bob:
[root@server1 ~]# ./argument lisa lori bob
The first argument is lisa
The second argument is lori
The third argument is bob
[root@server1 ~]#

Если вы попытались запустить пример кода, то могли заметить, что его содержимое не идеально. Если вы используете три аргумента при выполнении скрипта из листинга 1, он будет работать отлично. Если используете только два аргумента, третий выводится без значения $3. Если же используете четыре аргумента, четвёртое значение (которое будет храниться в $4) никогда не отобразится. Итак, если вы хотите использовать аргументы, вам лучше использовать более гибкий подход.

Листинг 2
#!/bin/bash
# run this script with a few arguments echo you have entered $# arguments
for i in $@
 do
       echo $i
 done
exit 0

В Листинге 2 представлены два новых элемента, которые относятся к аргументам:
  • $# - это счетчик, который показывает, сколько аргументов было использовано при запуске скрипта.
  • $@ - список всех аргументов, которые использовались при запуске скрипта.
Чтобы перечислить аргументы, которые использовались при запуске этого скрипта, используется цикл for. В циклах for команды выполняются до тех пор, пока условие истинно.В этом сценарии условие for i in $@ означает «для каждого аргумента»  Каждый раз, когда сценарий проходит цикл, значение из переменной $@ присваивается переменной $i.

Итак, пока есть аргументы, тело сценария выполняется.

Тело цикла for всегда начинается с do и закрывается done, а между этими двумя ключевыми словами перечисляются команды, которые необходимо выполнить. Таким образом, пример сценария будет использовать echo для отображения значения каждого аргумента и останавливаться, когда больше нет доступных аргументов.  

Давайте попробуем воспользоваться скриптом из листинга 2 в этом примере:
  1. Введите vi argument, чтобы создать файл argument и скопируйте содержимое из скрипта листинга 2 в этот файл.
  2. Сохраните файл и сделайте его исполняемым.
  3. Запустите команду ./argument a b c. Вы увидите, что отобразятся три строки.
  4. Запустите команду ./argument a b c d e f. Вы увидите, что помимо a b c отобразятся и d e f.

Переменные

Переменная — это метка, которая используется для обозначения определённого места в памяти, которое содержит определённое значение. Переменные могут быть определены статически с помощью NAME=value или динамическим способом. Существует два решения для динамического определения переменной:
  • Используйте ключевое слово read в скрипте, чтобы запросить данные у пользователя, запускающего скрипт.
  • Используйте подстановку команд, чтобы использовать результат команды и назначить его переменной. Например, команда date +%d-%m-%y показывает текущую дату в формате день-месяц-год. Чтобы сделать это в сценарии, вы можете использовать TODAY=$(date +%d-%m-%y). Для подстановки команд вам просто нужно поместить команду, результат которой вы хотите использовать, между скобками.
В предыдущем разделе о позиционных параметрах вы узнали, как присваивать аргументы переменным при запуске скрипта. В некоторых случаях может быть более эффективно запрашивать информацию, когда вы обнаружите, что чего-то существенного не хватает. Сценарий в ниже показывает, как это сделать.

Листинг 3. Пример скрипта, использующего команду read
#!/bin/bash
if [ -z $1 ]; then
     echo enter a text
     read TEXT
else
     TEXT=$1
fi
echo you have entered the text $TEXT
exit 0

В сценарии листинга 3 оператор if ... then ... else ... fi используется для проверки существования аргумента $1. Это делается с помощью test (test — это отдельная команда). Команда test может быть написана двумя способами: test или [ ... ]. В примере строка if [ -z $1 ] ... выполняется, чтобы увидеть тест (проверку) -z $1.

-z test проверяет, существует или нет $1. Иначе говоря, строка if [ -z $1 ] проверяет, является ли $1 пустым, что означает, что при запуске этого сценария не было предоставлено никаких аргументов. Если это так, команды после оператора then выполняются.

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

Обратите внимание, что оператор then следует сразу за test. Это возможно, потому что используется точка с запятой (;). Точка с запятой является разделителем команд и может заменить новую строку в скрипте.

В операторе then выполняются две команды: команда echo, которая отображает сообщение на экране, и команда read.
Команда read останавливает сценарий, чтобы пользовательский ввод мог быть обработан и сохранен в переменной TEXT. Поэтому read TEXT помещает все введённые пользователем данные в переменную TEXT, которая будет использоваться позже в скрипте.

Следующая часть представлена оператором else. Команды после оператора else выполняются во всех других случаях, что в данном случае означает «иначе, если аргумент был предоставлен». Если это так, то определяется переменная TEXT и ей присваивается текущее значение $1.

Обратите внимание, как определяется переменная: непосредственно после имени переменной стоит знак =, за которым следует $1. Обратите внимание, что вы никогда не должны использовать пробелы при определении переменных.

Затем цикл if замкнут с помощью оператора fi. После завершения цикла if вы точно знаете, что переменная TEXT определена и имеет значение. Предпоследняя строка скрипта считывает значение переменной TEXT и отображает это значение в STDOUT с помощью команды echo. Обратите внимание, что для запроса текущего значения переменной ссылается на имя переменной, начиная со знака $ перед ним.

Вы можете попрактиковаться на этом примере при работе с вводом.
  1. Откройте редактор и создайте файл с именем text. Введите содержимое кода из листинга 3 в этот файл.
  2. Запишите файл на диск и выполните chmod +x ./text, чтобы сделать его исполняемым.
  3. Запустите скрипт, выполнив ./text и без дополнительных аргументов. Вы увидите, что он запрашивает ввод.
  4. Запустите скрипт, используя "hello" в качестве аргумента (./text hello). Результат отобразит "you have entered the text hello" в STDOUT.

Использование условий и циклов

Как вы уже видели, в скрипте могут использоваться условные операторы. Эти условные операторы выполняются только в том случае, если определённое условие выполняется. В bash есть несколько условных операторов и циклов, которые часто используются.
  • if .. then .. else - используется для выполнения кода, если определенное условие выполняется
  • for - используется для выполнения команд для диапазона значений
  • while - используется для выполнения кода, если выполняется определенное условие
  • before - используется для выполнения кода, пока не выполнено определенное условие
  • case - используется для оценки ограниченного количества конкретных значений

if then else

Конструкция if then elseявляется общей для оценки конкретных условий. Вы уже видели пример с ним. Этот условный оператор часто используется вместе с командой test. Эта команда позволяет вам проверять многие вещи: например, не только, существует ли файл, но и сравнивать файлы, сравнивать целые числа и многое другое.
Подробнее о test можно узнать в справочнике командой man test.
Основная конструкция if есть if ... then ... fi. Она сравнивает одно условие, как показано в следующем примере:
if [ -z $1 ]
then
     echo no value provided
fi

В листинге 3 вы увидели, как можно оценить два условия, включая else в выражении. В листинге 4 показано, как можно оценить несколько условий от if до else. Это полезно, если нужно проверить много разных значений. Обратите внимание, что в этом примере также используются несколько команд test.

Листинг 4. Пример с if then else
#!/bin/bash
# run this script with one argument
# the goal is to find out if the argument is a file or a directory
if [ -f $1 ]
then
    echo "$1 is a file"
elif [ -d $1 ]
then
    echo "$1 is a directory"
else
    echo "I do not know what \$1 is"
fi
exit 0

|| и &&

Вместо написания полных операторов if ... then вы можете использовать логические операторы || а также &&. || является логическим "ИЛИ" и выполнит вторую часть оператора, только если первая часть не верна; && является логическим "И" и выполнит вторую часть оператора только в том случае, если первая часть верна.

Рассмотрим эти две строки:
[ -z $1 ] && echo no argument provided

ping -c 1 8.8.8.8 2>/dev/null || echo node is not available

В первом примере выполняется проверка, чтобы увидеть, пуст ли $1. Если эта проверка верна (что, в основном, означает, что команда завершается с кодом выхода 0), выполняется вторая команда.

Во втором примере команда ping используется для проверки доступности хоста.
В этом примере используется логическое "ИЛИ" для вывода текста "node is not available" в случае неудачной команды ping. Вы обнаружите, что часто вместо условного оператора if будут использоваться && и ||. В упражнении ниже вы можете попрактиковаться в использовании условных операторов, используя либо if ... then ... else, либо && и ||.

Упражнение. Использование if ... then ... else

В этом упражнении вы поработаете над сценарием, который проверяет что является файлом, а что каталогом.
  1. Запустите редактор и создайте скрипт с именем filechk.
  2. Скопируйте содержимое из листинга 4 в этот скрипт.
  3. Запустите с ним пару тестов, такие как ./filechk /etc/hosts, ./filechck /usr, ./filechk non-existing-file.

Цикл for

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

Листинг 5
#!/bin/bash
#
for (( COUNTER=100; COUNTER>1; COUNTER-- )); do
     echo $COUNTER
done
exit 0

Цикл for всегда начинается с ключевого слова for, за которым следует условие, которое необходимо проверить. Затем следует ключевое слово do, за которым следуют команды, которые должны быть выполнены, если условие истинно, завершается цикл с помощью ключевого слова done.

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

Маленькое пояснение
Внутри (( ... )) вычисляются арифметические выражения и возвращается их результат. Например, в простейшем случае, конструкция  a=$(( 5 + 3 )) присвоит переменной «a» значение выражения «5 + 3», или 8. Кроме того, двойные круглые скобки позволяют работать с переменными в стиле языка C.

Сначала переменная инициализируется значением 100, и до тех пор, пока значение больше 1, в каждой итерации вычитается 1. Пока условие истинно, значение переменной $COUNTER отображается с использованием команды echo. В листинге 6 вы можете увидеть один из моих любимых однострочников с for. Диапазон определяется на этот раз как последовательность чисел, начиная со 100 и доходя до 104.

Листинг 6
for i in {100..104}; do ping –c 1 192.168.4.$i >/dev/null && echo 192.168.4.$i is up; done

Обратите внимание, как определяется диапазон: сначала вы указываете первое число, затем две точки и указываете последнее число в диапазоне. При этом с for i in для каждого из этих номеров присваивается переменная i. Каждое из этих чисел присваивается переменной и затем выполняется команда ping, где опция -c 1 гарантирует, что отправляется только один запрос.

Результат выполнения команды ping не учитывается, поэтому её вывод перенаправляется в /dev/null. На основании состояния выхода команды ping выполняется часть выражения за &&. Таким образом, если хост доступен, отображается строка, указывающая, что он работает.

Понимание while и until

Если оператор for, о котором вы только что прочитали, полезен для работы с диапазонами элементов, то оператор while полезен тогда, когда вы хотите отслеживать что-то вроде доступности процесса. Также существует оператор until, который выполняется до тех пор, пока проверяемое условие ложно. В листинге 7 вы можете прочитать, как while используется для мониторинга активности процесса.

Листинг 7
#!/bin/bash
#
# usage: monitor <processname>
while ps aux | grep $1 | grep -v grep > /dev/tty11
do
     sleep 5
done

clear
echo your process has stopped
logger $1 is no longer present
mail -s "process $1 has stopped" root < .

Сценарий в листинге 7 состоит из двух частей. Во-первых, есть цикл while. Во-вторых, есть всё, что нужно выполнить, когда цикл while больше не оценивается как true.

Ядром цикла while является команда ps, которая имеет значение $1. Обратите внимание на использование grep -v grep, который исключает строки, содержащие команду grep, из результата. Помните, что команда ps будет включать в себя все запущенные процессы, включая команду grep, которой передаётся вывод команды ps. Это может привести к ложному положительному совпадению.

Вывод команды ps aux перенаправляются в /dev/tty11. Это позволяет позже прочитать результаты из tty11, если это необходимо, но они не отображаются по умолчанию.

После операторов while следуют команды, которые необходимо выполнить, если проверяемое условие истинно. В данном случае это команда sleep 5, которая приостанавливает выполнение скрипта на 5 секунд. Пока условие оператора while истинно, цикл продолжает выполняться. Если же условие ложно (что в данном случае означает, что процесс больше не доступен), то цикл останавливается и могут выполняться команды, следующие за ним.

Вам должны быть знакомы все эти команды, кроме последней. В строке mail -s “process $1 has stopped” root < . сообщение отправляется пользователю root с использованием внутренней почтовой системы, которая по умолчанию работает на Linux. Команда mail принимает в качестве первого аргумента тему сообщения, указанную с помощью опции -s.

Обратите внимание на < . в конце команды.
Обычно при использовании команды mail в интерактивном режиме открывается редактор, в котором можно написать тело сообщения. Этот редактор закрыт, предоставляя строку, которая имеет только точку. В этой команде точка предоставляется путём перенаправления STDIN. Это позволяет обрабатывать сообщение без каких-либо дополнительных требований к пользовательской активности.

Цикл while - это противоположность циклу until, пример которого приведён в листинге 8. until запускает итерацию, которая длится до тех пор, пока условие не станет истинным. В листинге 8 он используется для фильтрации выходных данных команды users по вхождению $1, которое будет именем пользователя. Пока эта команда не верна, итерация продолжается. Когда имя пользователя найдено в выводе пользователей, итерация закрывается, и после цикла until выполняются остальные команды.

Листинг 8
#!/bin/bash
#
until users | grep $1 > /dev/null
do
     echo $1 is not logged in yet
     sleep 5
done
echo $1 has just logged in
mail -s "$1 has just logged in" root < .

Понимание case

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

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

Листинг 9
case "$1" in
     start)
          start;;
     stop)
          rm -f $lockfile
          stop;;
     restart)
          restart;;
     reload)
          reload;;
     status)
          status
          ;;
     *)
          echo "Usage: $0 (start|stop|restart|reload|status)"
          ;;
esac

case имеет несколько особенностей. Сначала идет строка - case последовательность in. Затем следует список всех возможных значений, которые необходимо оценить. Каждый элемент закрывается скобкой ). Затем следует список команд, которые необходимо выполнить, если использовался конкретный аргумент. Список команд закрывается двойной точкой с запятой ;; может использоваться непосредственно после последней команды и может использоваться в отдельной строке.

Также обратите внимание, что *) относится ко всем другим параметрам, не указанным ранее. Это "всеохватывающий" оператор.

Цикл итераций case завершается оператором esac.

Обратите внимание, что последовательности в case выполняются по порядку. Когда будет выполнено первое совпадение, оператор case не будет ничего оценивать. В рамках оценки могут использоваться шаблоны, подобные шаблону. Это показано в *) последовательности, которая соответствует всему. Но вы также можете использовать последовательности, такие как start|Start|START), чтобы соответствовать использованию другого case.

Отладка скриптов в Bash

Когда скрипт не делает того, что вы ожидаете, полезно выполнить некоторую отладку. Для начала, попробуйте выполнить его как аргумент команды bash -x. Это покажет вам построчно, что пытается сделать скрипт, а также конкретные ошибки, если он не работает как надо. В листинге 10 показан пример использования bash -x, где сразу становится ясно, что команда grep не знает, что она должна делать, так как пропущен аргумент для её работы.

[root@server1 ~]# bash -x 319.sh
+ grep
Usage: grep [OPTION]... PATTERN [FILE]...
Try 'grep --help' for more information.
+ users
+ echo is not logged in yet is not logged in yet
+ sleep 5

Подводим итоги

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

Добавить комментарий